From 2c565980e3208a49e0cca517ae383a2050065b23 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:06:55 +0800 Subject: [PATCH 1/5] hal: Add halcompupdate to migrate .comp files to the new HAL API Out-of-tree .comp components using the legacy HAL types (float, bit, s32, u32, s64, u64, signed, unsigned) and direct pin/param assignment stop working when the HAL API break is performed (#4099, #4247). halcompupdate rewrites them to the new API automatically: * declaration types are converted: float->real, bit->bool, s32->si32, u32->ui32, s64->sint, u64->uint, signed->si32, unsigned->ui32 ('port' is left alone, it has no new-style replacement yet) * writes to out/io pins and to params become _set(...) calls, including compound assignments, ++/--, array pins, chained assignments, *_ptr dereferences and writes inside #define macros (macro parameters shadow same-named pins) * legacy C types are modernized (double/real_t -> rtapi_real, hal_bit_t -> volatile rtapi_bool, ...) * reads are unchanged and pins are never renamed, so existing HAL configurations keep working Constructs that cannot be converted safely are left unchanged with a warning for manual conversion: taking the address of a pin/param, direct use of the legacy hal_pin_*_new/hal_param_*_new creation API, postfix ++/-- whose value is used, and array indices with side effects. In-place rewriting is atomic (temp file + rename) and keeps a .bak backup created with O_EXCL|O_NOFOLLOW. halcompile now warns once per deprecated type, pointing to halcompupdate(1), at the spot previously marked for this warning. Docs: migration section in comp.adoc, new halcompupdate(1) manpage, SEE ALSO in halcompile(1). Regression test in tests/halcompile/update-api. Validated by converting all in-tree components from master: 119/124 compile (the other 5 need in-tree headers and fail identically for the already-converted versions), and the output matches the hand conversions in #4247 functionally. --- debian/linuxcnc-uspace-dev.install | 1 + debian/linuxcnc-uspace-dev.manpages | 1 + docs/src/hal/comp.adoc | 60 +- docs/src/man/man1/halcompile.1.adoc | 3 + docs/src/man/man1/halcompupdate.1.adoc | 98 ++ src/hal/utils/Submakefile | 8 +- src/hal/utils/halcompile.g | 15 +- src/hal/utils/halcompupdate.py | 1060 +++++++++++++++++ tests/halcompile/update-api/expected | 0 tests/halcompile/update-api/test.sh | 99 ++ .../update-api/test_update_api.comp | 32 + 11 files changed, 1371 insertions(+), 6 deletions(-) create mode 100644 docs/src/man/man1/halcompupdate.1.adoc create mode 100644 src/hal/utils/halcompupdate.py create mode 100644 tests/halcompile/update-api/expected create mode 100644 tests/halcompile/update-api/test.sh create mode 100644 tests/halcompile/update-api/test_update_api.comp diff --git a/debian/linuxcnc-uspace-dev.install b/debian/linuxcnc-uspace-dev.install index 199dae9fcc0..fd54c7469d9 100644 --- a/debian/linuxcnc-uspace-dev.install +++ b/debian/linuxcnc-uspace-dev.install @@ -1,4 +1,5 @@ usr/bin/halcompile +usr/bin/halcompupdate usr/bin/modcompile usr/include/linuxcnc usr/lib/liblinuxcnc.a diff --git a/debian/linuxcnc-uspace-dev.manpages b/debian/linuxcnc-uspace-dev.manpages index babd31b3653..bbabdc1080c 100644 --- a/debian/linuxcnc-uspace-dev.manpages +++ b/debian/linuxcnc-uspace-dev.manpages @@ -1,2 +1,3 @@ usr/share/man/man1/halcompile.1 +usr/share/man/man1/halcompupdate.1 usr/share/man/man3/* diff --git a/docs/src/hal/comp.adoc b/docs/src/hal/comp.adoc index aa6d5816bb5..943a464eefb 100644 --- a/docs/src/hal/comp.adoc +++ b/docs/src/hal/comp.adoc @@ -267,8 +267,14 @@ In this case, r-strings are particularly useful, because the backslashes in an r r"\fIexample\fB" ---- -* 'TYPE' - One of the HAL types: 'bit', 's32', 'u32', 's64', 'u64' or 'float'. - The names 'signed' and 'unsigned' may also be used for 's32' and 'u32' but 's32' and 'u32' are preferred. +* 'TYPE' - One of the HAL types: 'real', 'bool', 'si32', 'ui32', 'sint' or 'uint'. + 'real' is a floating point value, 'bool' a boolean, 'si32' and 'ui32' are 32-bit signed and unsigned integers, + and 'sint' and 'uint' are 64-bit signed and unsigned integers. + The legacy types 'float' (for 'real'), 'bit' (for 'bool'), 's32' (for 'si32'), 'u32' (for 'ui32'), + 's64' (for 'sint') and 'u64' (for 'uint') are still accepted, as are the aliases 'signed' and 'unsigned' + (for 'si32' and 'ui32'), but they are deprecated, 'halcompile' will warn about them, + and they will be removed when the HAL API break is performed. + See <>. * 'PINDIRECTION' - One of the following: 'in', 'out', or 'io'. A component sets a value for an 'out' pin, it reads a value from an 'in' pin, and it may read or set the value of an 'io' pin. * 'PARAMDIRECTION' - One of the following: 'r' or 'rw'. A component sets a value for a 'r' parameter, and it may read or set the value of a 'rw' parameter. @@ -470,6 +476,15 @@ The details of `struct __comp_state` and these macros may change from one versio + When the item is a conditional item, it is only legal to refer to it when its 'condition' evaluated to a nonzero value. +* `pin_name_set(`__value__`)` or `param_name_set(`__value__`)` - For each 'out' or 'io' pin and each parameter + of a new-style type ('real', 'bool', 'si32', 'ui32', 'sint', 'uint'), there is a macro which sets the value of the pin or parameter. + For arrays the form is 'pin_name_set(idx, value)'. + With the new-style types, assigning to an 'out' or 'io' pin or to a parameter with plain C assignment is not possible; + the '_set' macro must be used instead (reading stays transparent through the bare name). + The '_set' macro evaluates to the value that was set, so chained use like 'a_set(b_set(x))' works. + There is also a 'pin_name_ptr' macro, but with new-style types it evaluates to an opaque reference + which can only be used with the 'hal_get_*' and 'hal_set_*' functions; it cannot be dereferenced. + * 'variable_name' - For each variable 'variable_name' there is a macro which allows the name to be used on its own to refer to the variable. When 'variable_name' is an array, the normal C-style subscript is used: 'variable_name[idx]'. * 'data' - If "option data" is specified, this macro allows access to the instance data. @@ -478,6 +493,47 @@ When the item is a conditional item, it is only legal to refer to it when its 'c This macro iterates over all the defined instances. Inside the body of the loop, the 'pin_name', 'parameter_name', and 'data' macros work as they do in realtime functions. +[[sub:hal-comp-migration]] +== Migrating to the new HAL API + +HAL is moving from direct memory access of pin and parameter values to strongly typed getter/setter access. +New-style declaration types replace the legacy types: + +[cols="1,1,3",options="header"] +|=== +| Legacy type | New type | Underlying C type +| 'float' | 'real' | 'rtapi_real' (floating point) +| 'bit' | 'bool' | 'rtapi_bool' (boolean) +| 's32' | 'si32' | 'rtapi_s32' (32-bit signed) +| 'u32' | 'ui32' | 'rtapi_u32' (32-bit unsigned) +| 's64' | 'sint' | 'rtapi_sint' (64-bit signed) +| 'u64' | 'uint' | 'rtapi_uint' (64-bit unsigned) +| 'signed' | 'si32' | 'rtapi_s32' (32-bit signed) +| 'unsigned' | 'ui32' | 'rtapi_u32' (32-bit unsigned) +|=== + +The legacy types still work, but 'halcompile' warns about them and they will be removed when the HAL API break is performed. +With the new-style types: + +* reading a pin or parameter is unchanged (the bare name, or 'name(idx)' for arrays, evaluates to the value); +* writing an 'out' or 'io' pin or a parameter requires the generated 'name_set(value)' macro + (or 'name_set(idx, value)' for arrays) instead of plain assignment; +* 'name_ptr' is an opaque reference for use with 'hal_get_*'/'hal_set_*' and can no longer be dereferenced. + +Existing components can be migrated automatically with the 'halcompupdate' tool: + +---- +halcompupdate mycomponent.comp # show a diff of the required changes +halcompupdate -i mycomponent.comp # rewrite the file in place (keeps a .bak) +---- + +The tool converts the declaration types and rewrites writes to pins and parameters +(including compound assignments and dereferences of 'name_ptr') to the '_set' form. +It deliberately does not rename pins or parameters, because their names are part of the HAL configuration interface. +Components that use the legacy 'hal_pin_*_new'/'hal_param_*_new' creation functions or that pass +the address of a pin ('&name') to helper functions cannot be converted automatically; +'halcompupdate' prints a warning for those cases so they can be converted by hand. + == Components with one function If a component has only one function and the string `"FUNCTION"` does not appear anywhere after `;;`, diff --git a/docs/src/man/man1/halcompile.1.adoc b/docs/src/man/man1/halcompile.1.adoc index c06cd1b8688..90f29ea4426 100644 --- a/docs/src/man/man1/halcompile.1.adoc +++ b/docs/src/man/man1/halcompile.1.adoc @@ -99,6 +99,9 @@ Extra arguments passed to the linker. == SEE ALSO +* *halcompupdate*(1) to migrate existing *.comp* files to the new HAL + pin/param API (new-style declaration types and *_set()* accessors) + * _Halcompile_ / _HAL Component Generator_ in the LinuxCNC documentation for a full description of the *.comp* syntax, along with examples diff --git a/docs/src/man/man1/halcompupdate.1.adoc b/docs/src/man/man1/halcompupdate.1.adoc new file mode 100644 index 00000000000..25d5ec28c34 --- /dev/null +++ b/docs/src/man/man1/halcompupdate.1.adoc @@ -0,0 +1,98 @@ += halcompupdate(1) + +== NAME + +halcompupdate - Migrate HAL .comp components to the new HAL pin/param API + +== SYNOPSIS + +*halcompupdate* [--in-place] [--no-backup] [--check] [--no-c-types] [--quiet] compfile... + +== DESCRIPTION + +*halcompupdate* converts HAL components written for the legacy HAL API +(pins and params declared with the types *float*, *bit*, *s32*, *u32*, +*s64*, *u64*, *signed* or *unsigned* and written with plain C assignment) +to the new HAL API, where pins and params are declared with the types +*real*, *bool*, *si32*, *ui32*, *sint* and *uint* and written with the +generated *_set(value)* accessor. + +The legacy types still work, but halcompile(1) warns about them and they +will be removed when the HAL API break is performed. + +The following transformations are applied: + +* Declaration types are replaced: *float* -> *real*, *bit* -> *bool*, + *s32* -> *si32*, *u32* -> *ui32*, *s64* -> *sint*, *u64* -> *uint*, + *signed* -> *si32*, *unsigned* -> *ui32*. The type *port* is not + converted yet. +* Assignments to *out* and *io* pins and to parameters are rewritten to + the *_set(...)* form, including compound assignments + (*name += x* becomes *name_set(name + (x))*), increments/decrements + and array pins (*name(i) = x* becomes *name_set(i, x)*). +* Dereferences of the old pin pointer macro are rewritten: + ***name_ptr*** reads become *(name)* and ***name_ptr = x*** writes + become *name_set(x)*. +* Legacy C types are modernized with their variable-use replacements + (*double* -> *rtapi_real*, *hal_float_t* -> *rtapi_real*, + *hal_bit_t* -> *rtapi_bool*, and so on). The *volatile* qualifier of + the legacy types is dropped on purpose: it existed for direct access + to HAL memory and was never right for variables. Pointers to the + legacy types referenced HAL memory and become opaque references in the + new API; those are warned about and left unchanged. Use + *--no-c-types* to skip this conversion. + +Reading pins and params is unchanged: the bare name (or *name(idx)* for +arrays) evaluates to the value in both APIs. + +Pin and parameter names are never changed, because they are part of the +HAL configuration interface used by .hal files. + +Components that use the legacy *hal_pin_*_new*/*hal_param_*_new* creation +functions directly, or that pass the address of a pin (*&name*) to helper +functions, cannot be converted automatically. *halcompupdate* prints a +warning for those cases so they can be converted by hand. + +== OPTIONS + +*compfile...*:: +One or more .comp files to convert. +Without other options, a unified diff of the required changes is printed. + +*-i*, *--in-place*:: +Rewrite the files in place. A backup with the suffix *.bak* is kept +unless *--no-backup* is given. + +*--no-backup*:: +With *--in-place*, do not keep a *.bak* backup. + +*--check*:: +Do not write anything; exit with status 1 if any file would be changed. +Useful in build systems to verify that components are migrated. + +*--no-c-types*:: +Only convert pin/param declarations and accesses; do not modernize +C types in the component body. + +*-q*, *--quiet*:: +Suppress warnings on stderr. + +== EXAMPLES + +Show what would change: + +---- +halcompupdate mycomponent.comp +---- + +Migrate in place: + +---- +halcompupdate -i mycomponent.comp +---- + +== SEE ALSO + +*halcompile*(1), the _Halcompile_ / _HAL Component Generator_ section in +the LinuxCNC documentation for a full description of the *.comp* syntax +and the new HAL API. diff --git a/src/hal/utils/Submakefile b/src/hal/utils/Submakefile index 856b8df389f..521ce3c7b58 100644 --- a/src/hal/utils/Submakefile +++ b/src/hal/utils/Submakefile @@ -111,6 +111,12 @@ endif $(ECHO) Copying python script $(notdir $@) $(Q)(echo '#!$(PYTHON)'; sed '1 { /^#!/d; }' $<) > $@.tmp && chmod +x $@.tmp && mv -f $@.tmp $@ +../bin/halcompupdate: ../bin/%: hal/utils/%.py + @$(ECHO) Syntax checking python script $(notdir $@) + $(Q)$(PYTHON) -m py_compile $< + $(ECHO) Copying python script $(notdir $@) + $(Q)(echo '#!$(PYTHON)'; sed '1 { /^#!/d; }' $<) > $@.tmp && chmod +x $@.tmp && mv -f $@.tmp $@ + ../bin/modcompile: ../bin/%: hal/drivers/mesa-hostmot2/modbus/%.py @$(ECHO) Syntax checking python script $(notdir $@) $(Q)$(PYTHON) -m py_compile $< @@ -126,7 +132,7 @@ endif $(ECHO) Copying python script $(notdir $@) $(Q)(echo '#!$(PYTHON)'; sed '1 { /^#!/d; }' $<) > $@.tmp && chmod +x $@.tmp && mv -f $@.tmp $@ -TARGETS += ../bin/halcompile ../bin/elbpcom ../bin/modcompile ../share/linuxcnc/mesa_modbus.c.tmpl ../bin/mesambccc +TARGETS += ../bin/halcompile ../bin/elbpcom ../bin/modcompile ../share/linuxcnc/mesa_modbus.c.tmpl ../bin/mesambccc ../bin/halcompupdate objects/%.py: %.g @mkdir -p $(dir $@) $(Q)$(YAPPS) $< $@ diff --git a/src/hal/utils/halcompile.g b/src/hal/utils/halcompile.g index 2690dc61df7..2a74a6dadb6 100644 --- a/src/hal/utils/halcompile.g +++ b/src/hal/utils/halcompile.g @@ -151,9 +151,11 @@ def parse(filename): dirmap = {'r': 'HAL_RO', 'rw': 'HAL_RW', 'in': 'HAL_IN', 'out': 'HAL_OUT', 'io': 'HAL_IO' } typemap = {'signed': 's32', 'unsigned': 'u32'} -deprmap = {'s32': 'signed', 'u32': 'unsigned'} -deprecated = ['s32', 'u32'] newtypes = ['bool', 'sint', 'uint', 'si32', 'ui32', 'real'] +# Old HAL declaration types and their new-style replacements. These types +# will be removed when the HAL API break is performed. +old2newtypes = {'float': 'real', 'bit': 'bool', 's32': 'si32', 'u32': 'ui32', + 's64': 'sint', 'u64': 'uint', 'signed': 'si32', 'unsigned': 'ui32'} def initialize(): global functions, params, pins, comp_name, names, docs, variables @@ -162,6 +164,7 @@ def initialize(): functions = []; params = []; pins = []; options = {}; variables = [] modparams = []; docs = []; includes = []; comp_name = None + deprecated_type_warnings.clear() names = {} @@ -207,8 +210,14 @@ def see_also(doc): def notes(doc): docs.append(('notes', doc)); +deprecated_type_warnings = set() + def type2type(type_): - # When we start warning about s32/u32 this is where the warning goes + if type_ in old2newtypes and type_ not in deprecated_type_warnings: + deprecated_type_warnings.add(type_) + Warn("HAL type '%s' is deprecated and will be removed at the HAL API " + "break; use '%s' instead. Run halcompupdate(1) to migrate " + "automatically." % (type_, old2newtypes[type_])) return typemap.get(type_, type_) def checkarray(name, array): diff --git a/src/hal/utils/halcompupdate.py b/src/hal/utils/halcompupdate.py new file mode 100644 index 00000000000..ab978b8176e --- /dev/null +++ b/src/hal/utils/halcompupdate.py @@ -0,0 +1,1060 @@ +#!/usr/bin/env python3 +# +# halcompupdate - migrate HAL .comp components to the new HAL pin/param API +# Copyright 2026 Luca Toniolo +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# The new HAL API replaces direct memory access to pins and params with +# strongly typed getter/setter access: +# +# * declaration types change: +# float -> real, bit -> bool, s32 -> si32, u32 -> ui32, +# s64 -> sint, u64 -> uint, signed -> si32, unsigned -> ui32 +# ('port' is not converted yet) +# * writing an out/io pin or a param is done with the generated +# _set(value) function instead of plain assignment +# * reading stays transparent (the bare name keeps working) +# * _ptr is no longer dereferenceable; *_ptr reads become +# () and *_ptr writes become _set(...) +# +# This tool rewrites a .comp file accordingly. By default it prints a +# unified diff; use --in-place to rewrite the file. + +import argparse +import difflib +import os +import re +import sys +import tempfile + +OLD2NEW = { + 'float': 'real', + 'bit': 'bool', + 's32': 'si32', + 'u32': 'ui32', + 's64': 'sint', + 'u64': 'uint', + 'signed': 'si32', + 'unsigned': 'ui32', +} + +# C types used with the old HAL API and their modern replacements for +# variable use. The volatile qualifier of the legacy hal_*_t types exists +# only because they were used for direct access to HAL memory; as variable +# types they were always wrong, so the plain rtapi_* forms are used here. +# Plain C 'float' is deliberately not mapped: it is not part of the HAL +# API and stays valid C (only 'variable' declarations are converted). +# Pointers to the legacy types are a different matter (they referenced +# HAL memory and become opaque hal_*_t references); those are warned +# about and left unchanged. +CTYPE_MAP = { + 'double': 'rtapi_real', + 'real_t': 'rtapi_real', + 'hal_float_t': 'rtapi_real', + 'hal_bit_t': 'rtapi_bool', + 'hal_s32_t': 'rtapi_s32', + 'hal_u32_t': 'rtapi_u32', + 'hal_s64_t': 'rtapi_s64', + 'hal_u64_t': 'rtapi_u64', +} + +# Mapping for 'variable' declarations. The comp grammar allows only a +# single-word type there, and instance variables are component-private +# memory, so the plain rtapi_* forms are used (no volatile qualifier). +# Old code frequently used HAL type names here ('float' for HAL floats); +# C 'float' variables holding HAL values must become rtapi_real. +# 'signed'/'unsigned' are *not* mapped: they are legitimate plain C types. +VAR_TYPE_MAP = { + 'double': 'rtapi_real', + 'real_t': 'rtapi_real', + 'float': 'rtapi_real', + 'hal_float_t': 'rtapi_real', + 'hal_bit_t': 'rtapi_bool', + 'hal_s32_t': 'rtapi_s32', + 'hal_u32_t': 'rtapi_u32', + 'hal_s64_t': 'rtapi_s64', + 'hal_u64_t': 'rtapi_u64', +} + +# HAL C types/macros that will disappear at the API break; used in bodies +LEGACY_C_TYPES = [ + 'hal_bit_t', 'hal_float_t', 'hal_s32_t', 'hal_u32_t', + 'hal_s64_t', 'hal_u64_t', 'hal_data_u', 'hal_pin_dir_t', + 'hal_param_dir_t', +] +LEGACY_TYPE_MACROS = [ + 'HAL_BIT', 'HAL_FLOAT', 'HAL_S32', 'HAL_U32', 'HAL_S64', 'HAL_U64', +] + +PIN_WRITABLE_DIRS = {'out', 'io'} +PARAM_WRITABLE_DIRS = {'rw', 'r'} + + +def to_c(name): + """Replicates halcompile's to_c(): HAL name -> C identifier.""" + name = re.sub(r"[-._]*#+", "", name) + name = name.replace("#", "").replace(".", "_").replace("-", "_") + return re.sub(r"_+", "_", name) + + +class Reporter: + def __init__(self, filename, quiet=False): + self.filename = filename + self.quiet = quiet + + def warn(self, msg, lineno=0): + if not self.quiet: + print("%s:%d: Warning: %s" % (self.filename, lineno or 0, msg), + file=sys.stderr) + + def error(self, msg): + raise SystemExit("%s:0: Error: %s" % (self.filename, msg)) + + +def lineno_of(text, pos): + return text.count("\n", 0, pos) + 1 + + +# --------------------------------------------------------------------------- +# Header parsing +# --------------------------------------------------------------------------- + +def split_statements(header): + """Split the declaration part (before ';;') into statements terminated + by ';', ignoring semicolons inside strings and triple-quoted docs. + Returns a list of (start, end) spans, end includes the ';'.""" + spans = [] + i, start, n = 0, 0, len(header) + while i < n: + c = header[i] + if header.startswith('"""', i) or header.startswith('r"""', i): + if header[i] == 'r': + i += 1 + i += 3 + while i < n: + if header[i] == '\\': + i += 2 + continue + if header.startswith('"""', i): + i += 3 + break + i += 1 + continue + if c == '"': + i += 1 + while i < n and header[i] != '"': + if header[i] == '\\': + i += 1 + i += 1 + i += 1 + continue + if c == "'": + i += 1 + while i < n and header[i] != "'": + if header[i] == '\\': + i += 1 + i += 1 + i += 1 + continue + if header.startswith('//', i): + j = header.find('\n', i) + i = n if j < 0 else j + 1 + continue + if header.startswith('/*', i): + j = header.find('*/', i + 2) + i = n if j < 0 else j + 2 + continue + if c == ';': + spans.append((start, i + 1)) + start = i + 1 + i += 1 + if start < n: + spans.append((start, n)) + return spans + + +DECL_RE = re.compile( + r'^(\s*(?:(?://[^\n]*|/\*.*?\*/)\s*)*)' # leading whitespace/comments + r'(pin|param)\s+(in|out|io|rw|r)\s+' + r'([A-Za-z][A-Za-z0-9]*)\s+' # HAL type + r'([#A-Za-z_][-#A-Za-z0-9_.]*)' # HAL name + r'(\s*\[)?', # array marker + re.S) + +VAR_RE = re.compile( + r'^(\s*(?:(?://[^\n]*|/\*.*?\*/)\s*)*)' # leading whitespace/comments + r'variable\s+' + r'([A-Za-z_][A-Za-z0-9_]*)\s+' # C type + r'\*?\s*[#A-Za-z_]', # (possibly starred) name + re.S) + + +class Decl: + def __init__(self, kind, direction, type_, name, is_array): + self.kind = kind # 'pin' or 'param' + self.direction = direction + self.type = type_ + self.name = name # HAL name + self.cname = to_c(name) + self.is_array = is_array + + @property + def writable(self): + if self.kind == 'pin': + return self.direction in PIN_WRITABLE_DIRS + return self.direction in PARAM_WRITABLE_DIRS + + +def parse_declarations(header): + """Find pin/param declarations, return (list of Decl, set of options).""" + decls = [] + options = set() + for start, end in split_statements(header): + stmt = header[start:end] + m = DECL_RE.match(stmt) + if m: + _, kind, direction, type_, name, array = m.groups() + decls.append(Decl(kind, direction, type_, name, bool(array))) + continue + mo = re.match(r'\s*option\s+([A-Za-z_][A-Za-z0-9_]*)', stmt) + if mo: + options.add(mo.group(1)) + return decls, options + + +def convert_header(header, rep, c_types=True, legacy_api=False): + """Replace old HAL types by new ones in pin/param declarations and + modernize C types in 'variable' declarations. Only the type token + itself is replaced, all other text (spacing, docs) is preserved. + Returns (new_header, changed, decls, options).""" + decls, options = parse_declarations(header) + if 'no_convenience_defines' in options: + rep.error("component uses 'option no_convenience_defines'; " + "automatic body conversion is not possible") + var_map = VAR_TYPE_MAP + if legacy_api: + # keep hal_*_t: their addresses are passed to legacy API calls + var_map = {k: v for k, v in VAR_TYPE_MAP.items() + if not k.startswith('hal_')} + edits = [] # (start, end, replacement) in header coordinates + for start, end in split_statements(header): + stmt = header[start:end] + m = DECL_RE.match(stmt) + if m: + type_ = m.group(4) + if type_ in OLD2NEW: + edits.append((start + m.start(4), start + m.end(4), + OLD2NEW[type_])) + continue + if c_types: + mv = VAR_RE.match(stmt) + if mv and mv.group(2) in var_map: + edits.append((start + mv.start(2), start + mv.end(2), + var_map[mv.group(2)])) + if not edits: + return header, False, decls, options + out = [] + prev = 0 + for s0, e0, repl in edits: + out.append(header[prev:s0]) + out.append(repl) + prev = e0 + out.append(header[prev:]) + return ''.join(out), True, decls, options + + +# --------------------------------------------------------------------------- +# Body rewriting +# --------------------------------------------------------------------------- + +def skip_ws_comments(s, i): + n = len(s) + while i < n: + if s[i] in ' \t\r\n': + i += 1 + elif s.startswith('//', i): + j = s.find('\n', i) + i = n if j < 0 else j + 1 + elif s.startswith('/*', i): + j = s.find('*/', i + 2) + i = n if j < 0 else j + 2 + else: + break + return i + + +def scan_string(s, i): + quote = s[i] + i += 1 + n = len(s) + while i < n: + if s[i] == '\\': + i += 2 + continue + if s[i] == quote: + return i + 1 + i += 1 + return n + + +def match_parens(s, i): + """i points at '('. Return index just past the matching ')'.""" + depth = 0 + n = len(s) + while i < n: + c = s[i] + if c == '"' or c == "'": + i = scan_string(s, i) + continue + if s.startswith('//', i): + j = s.find('\n', i) + i = n if j < 0 else j + 1 + continue + if s.startswith('/*', i): + j = s.find('*/', i + 2) + i = n if j < 0 else j + 2 + continue + if c in '([{': + depth += 1 + elif c in ')]}': + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return n + + +def read_assign_op(s, i): + """At position i (after optional whitespace already skipped), return + (op, end) if an assignment operator follows, else None.""" + for op in ('<<=', '>>='): + if s.startswith(op, i): + return op, i + 3 + c = s[i] if i < len(s) else '' + if c in '+-*/%&|^': + if s[i + 1:i + 2] == '=': + return c + '=', i + 2 + return None + if c == '=': + if s[i + 1:i + 2] == '=': + return None + return '=', i + 1 + return None + + +def read_expr(s, i): + """Read a C expression starting at i up to (not including) the + terminating ';', ',', unmatched ')'/'}' or ternary ':' at nesting + level zero. Returns (expr_text, end_index).""" + start = i + depth = 0 + ternary = 0 + n = len(s) + while i < n: + c = s[i] + if c == '"' or c == "'": + i = scan_string(s, i) + continue + if s.startswith('//', i): + j = s.find('\n', i) + i = n if j < 0 else j + 1 + continue + if s.startswith('/*', i): + j = s.find('*/', i + 2) + i = n if j < 0 else j + 2 + continue + if c in '([{': + depth += 1 + elif c in ')]}': + if depth == 0: + break + depth -= 1 + elif depth == 0: + if c == '?': + ternary += 1 + elif c == ':': + if ternary: + ternary -= 1 + else: + break + elif c == ';' or c == ',': + break + i += 1 + return s[start:i].strip(), i + + +IDENT_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_]*') + +# Index expressions that are guaranteed side-effect-free (plain identifier +# or numeric literal); required where the index is emitted more than once. +SIMPLE_IDX_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_]*|0x[0-9a-fA-F]+|[0-9]+)$') + +# Legacy pin/param creation calls used directly in hand-written C code. +# Components using these need manual conversion; converting the hal_*_t C +# types would break compilation of the calls themselves. +LEGACY_API_RE = re.compile( + r'\bhal_(?:pin|param)_(?:bit|float|s32|u32|s64|u64|port)_newf?\s*\(' + r'|\bhal_(?:pin|param)_new\s*\(') + +DEFINE_RE = re.compile( + r'#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)(\(([^)]*)\))?') + + +class BodyRewriter: + def __init__(self, body, decls, rep, c_types=True, legacy_api=False): + self.s = body + self.rep = rep + self.c_types = c_types + # When the code calls the legacy creation API directly, the hal_*_t + # types must stay (their addresses are passed to those functions); + # such components need manual conversion. + if legacy_api: + self.ctype_map = {'double': 'rtapi_real'} + else: + self.ctype_map = CTYPE_MAP + self.out = [] + # Only converted (old-type) decls need rewriting; new-type decls + # are already fine and 'port' keeps direct access. + conv = [d for d in decls if d.type in OLD2NEW] + self.writable = {d.cname: d for d in conv if d.writable} + self.readonly = {d.cname: d for d in conv if not d.writable} + # all converted decls (pins and params), for &-address checks + self.allconv = dict(self.readonly) + self.allconv.update(self.writable) + # _ptr only ever existed for pins, never for params + self.pinconv = {d.cname: d for d in conv if d.kind == 'pin'} + self.changed = False + self.last_sig = '' # last significant char copied to output + self.last_sig2 = '' # the one before it + self.last_ident = None # last identifier copied to output + self.fragment = False # True when rewriting a sub-expression + self.line_offset = 0 + + @classmethod + def for_fragment(cls, parent, text, exclude=(), line_offset=0): + """Create a rewriter for an expression fragment extracted from + parent, sharing its pin/param tables. 'exclude' lists identifiers + that shadow pins/params in the fragment (macro parameters).""" + sub = cls.__new__(cls) + sub.s = text + sub.rep = parent.rep + sub.c_types = parent.c_types + sub.ctype_map = parent.ctype_map + sub.out = [] + sub.writable = {k: v for k, v in parent.writable.items() + if k not in exclude} + sub.readonly = {k: v for k, v in parent.readonly.items() + if k not in exclude} + sub.allconv = {k: v for k, v in parent.allconv.items() + if k not in exclude} + sub.pinconv = {k: v for k, v in parent.pinconv.items() + if k not in exclude} + sub.changed = False + sub.last_sig = '' + sub.last_sig2 = '' + sub.last_ident = None + sub.fragment = True + sub.line_offset = line_offset + return sub + + def rewrite_expr(self, expr): + """Recursively rewrite an extracted expression (handles chained + assignments like 'x = y = z').""" + sub = BodyRewriter.for_fragment(self, expr) + out = sub.run() + self.changed = self.changed or sub.changed + return out + + def emit(self, text): + """Emit significant code, tracking the last tokens seen.""" + self.out.append(text) + for c in text: + if c not in ' \t\r\n': + self.last_sig2 = self.last_sig + self.last_sig = c + + def emit_raw(self, text): + """Emit text that must not influence token tracking (comments, + strings, whitespace, preprocessor lines).""" + self.out.append(text) + + def pop_star(self): + """Remove a trailing '*' (the dereference operator) from the output, + keeping surrounding whitespace. Walks the output chunks backwards + and only touches the affected tail chunk. Returns False if the + last significant output char is not a plain single '*'.""" + for idx in range(len(self.out) - 1, -1, -1): + chunk = self.out[idx] + stripped = chunk.rstrip() + if not stripped: + continue # whitespace-only chunk + if not stripped.endswith('*'): + return False + if len(stripped) > 1 and stripped[-2] == '*': # '**' double deref + return False + pos = len(stripped) - 1 + self.out[idx] = chunk[:pos] + chunk[pos + 1:] + return True + return False + + def idx_has_pin_write(self, idx): + """True if the array index expression assigns to, or increments/ + decrements, a converted writable pin/param (side effect whose + semantics cannot be preserved by automatic rewriting).""" + for name in self.writable: + m = re.search(r'\b%s\b' % re.escape(name), idx) + while m: + k = skip_ws_comments(idx, m.end()) + if (read_assign_op(idx, k) or idx.startswith('++', k) + or idx.startswith('--', k)): + return True + m = re.search(r'\b%s\b' % re.escape(name), idx, m.end()) + return False + + def line(self, pos): + return self.line_offset + lineno_of(self.s, pos) + + def run(self): + s = self.s + n = len(s) + i = 0 + at_line_start = True + while i < n: + c = s[i] + if s.startswith('//', i): + j = s.find('\n', i) + j = n if j < 0 else j + self.emit_raw(s[i:j]) + i = j + continue + if s.startswith('/*', i): + j = s.find('*/', i + 2) + j = n if j < 0 else j + 2 + self.emit_raw(s[i:j]) + i = j + continue + if c == '\n': + self.emit_raw(c) + i += 1 + at_line_start = True + continue + if c in ' \t\r': + self.out.append(c) + i += 1 + continue + if c == '#' and at_line_start: + j = i + while True: + e = s.find('\n', j) + e = n if e < 0 else e + if e > j and s[e - 1] == '\\' and e < n: + j = e + 1 + continue + break + line = s[i:e] + i = e + at_line_start = False + # #define bodies may contain pin/param writes that must be + # converted; macro parameters shadow same-named pins. + dm = DEFINE_RE.match(line) + if dm: + self.handle_define(line, dm, i) + else: + self.emit_raw(line) + continue + at_line_start = False + if c == '"' or c == "'": + j = scan_string(s, i) + self.emit_raw(s[i:j]) + i = j + continue + m = IDENT_RE.match(s, i) + if m: + ident = m.group(0) + # struct member access (.name, ->name) is never a pin + if self.last_sig == '.' or (self.last_sig == '>' and self.last_sig2 == '-'): + self.emit(ident) + self.last_ident = ident + i = m.end() + continue + if self.c_types and ident in self.ctype_map: + # keep 'long double' intact + if ident == 'double' and self.last_ident == 'long': + self.emit(ident) + elif ident.startswith('hal_') and s[skip_ws_comments(s, m.end()):].startswith('*'): + self.rep.warn("pointer to legacy HAL type '%s'; pointers " + "into HAL memory become opaque references " + "in the new API - left as-is, convert to " + "hal_get_*/hal_set_* manually" % ident, + self.line(i)) + self.emit(ident) + else: + self.emit(self.ctype_map[ident]) + self.changed = True + self.last_ident = ident + i = m.end() + continue + if ident.endswith('_ptr') and ident[:-4] in self.pinconv: + i = self.handle_ptr(ident[:-4], i, m.end()) + self.last_ident = None + continue + if ident in self.writable: + i = self.handle_writable(ident, i, m.end()) + self.last_ident = None + continue + if ident in self.readonly: + k = skip_ws_comments(s, m.end()) + if read_assign_op(s, k) or s.startswith('++', k) or s.startswith('--', k): + self.rep.warn("assignment to input pin '%s' left as-is " + "(was already invalid)" % ident, self.line(i)) + self.emit(ident) + self.last_ident = ident + i = m.end() + continue + self.emit(ident) + self.last_ident = ident + i = m.end() + continue + if s.startswith('++', i) or s.startswith('--', i): + i = self.handle_prefix_incdec(i) + self.last_ident = None + continue + if c == '&' and not s.startswith('&&', i) and not s.startswith('&=', i): + k = skip_ws_comments(s, i + 1) + mi = IDENT_RE.match(s, k) + if mi and mi.group(0) in self.allconv: + self.rep.warn("address of pin/param '%s' is taken; with the new " + "API '%s_ptr' is an opaque reference - convert the " + "called code to hal_get_*/hal_set_* manually" + % (mi.group(0), mi.group(0)), self.line(i)) + self.emit(c) + self.last_ident = None + i += 1 + continue + self.emit(c) + if c not in ' \t\r\n': + self.last_ident = None + i += 1 + if not self.fragment: + self.check_legacy_refs() + return ''.join(self.out) + + # -- token handlers ---------------------------------------------------- + + def emit_gap(self, a, b): + """Preserve a skipped whitespace/comment region if it contains a + comment (pure whitespace may be dropped when rewriting).""" + if a < b: + gap = self.s[a:b] + if '//' in gap or '/*' in gap: + self.emit_raw(gap) + + def value_discarded(self): + """True when the expression value at the current position is + discarded (statement context), so postfix ++/-- may be rewritten + with prefix semantics.""" + return self.last_sig in ('', ';', '{', '}') + + def rewrite_index(self, ident, idx, pos): + """Rewrite an array index expression. Returns the rewritten + index, or None when it contains a pin/param write whose semantics + cannot be preserved (a warning is emitted then).""" + if self.idx_has_pin_write(idx): + self.rep.warn("array index of '%s' modifies a pin/param; " + "left as-is - convert manually" % ident, pos) + return None + return self.rewrite_expr(idx) + + def handle_writable(self, ident, start, end): + """A writable pin/param identifier. Convert writes to _set().""" + s = self.s + decl = self.writable[ident] + k = skip_ws_comments(s, end) + idx = None + after = k + gaps = [] + if decl.is_array and k < len(s) and s[k] == '(': + close = match_parens(s, k) + k2 = skip_ws_comments(s, close) + if (read_assign_op(s, k2) or s.startswith('++', k2) + or s.startswith('--', k2)): + idx = s[k + 1:close - 1].strip() + gaps = [(end, k), (close, k2)] + after = k2 + else: + # read access name(index): keep as-is; the index expression + # is processed normally by the main loop + self.emit(s[start:end]) + self.emit_raw(s[end:k]) + return k + else: + gaps = [(end, k)] + op = read_assign_op(s, after) + if op: + opstr, opend = op + if idx is not None: + idx = self.rewrite_index(ident, idx, self.line(start)) + if idx is None: + self.emit(s[start:opend]) + return opend + if (opstr != '=' and not SIMPLE_IDX_RE.match(idx)): + self.rep.warn("compound assignment to '%s' has an index " + "with side effects; left as-is - convert " + "manually" % ident, self.line(start)) + self.emit(s[start:opend]) + return opend + expr, exprend = read_expr(s, skip_ws_comments(s, opend)) + if not expr: + self.rep.warn("could not parse expression in assignment to '%s'; " + "left as-is" % ident, self.line(start)) + self.emit(s[start:opend]) + return opend + expr = self.rewrite_expr(expr) + for a, b in gaps: + self.emit_gap(a, b) + target = "%s(%s)" % (ident, idx) if idx is not None else ident + args = "%s, " % idx if idx is not None else "" + if opstr == '=': + new = "%s_set(%s%s)" % (ident, args, expr) + else: + new = "%s_set(%s%s %s (%s))" % (ident, args, target, + opstr[0], expr) + self.emit(new) + self.changed = True + return exprend + if s.startswith('++', after) or s.startswith('--', after): + sign = '+' if s.startswith('++', after) else '-' + if not self.value_discarded(): + self.rep.warn("postfix %s on pin/param '%s' changes value " + "semantics (setter yields the new value); " + "left as-is - convert manually" + % (s[after:after + 2], ident), self.line(start)) + self.emit(s[start:after + 2]) + return after + 2 + if idx is not None: + idx = self.rewrite_index(ident, idx, self.line(start)) + if idx is None: + self.emit(s[start:after + 2]) + return after + 2 + if not SIMPLE_IDX_RE.match(idx): + self.rep.warn("increment/decrement of '%s' has an index " + "with side effects; left as-is - convert " + "manually" % ident, self.line(start)) + self.emit(s[start:after + 2]) + return after + 2 + for a, b in gaps: + self.emit_gap(a, b) + target = "%s(%s)" % (ident, idx) if idx is not None else ident + args = "%s, " % idx if idx is not None else "" + self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) + self.changed = True + return after + 2 + # plain read access: unchanged + self.emit(s[start:end]) + self.emit_raw(s[end:after]) + return after + + def handle_prefix_incdec(self, i): + s = self.s + sign = '+' if s.startswith('++', i) else '-' + k = skip_ws_comments(s, i + 2) + m = IDENT_RE.match(s, k) + if m and m.group(0) in self.writable: + ident = m.group(0) + decl = self.writable[ident] + after = skip_ws_comments(s, m.end()) + idx = None + if decl.is_array and after < len(s) and s[after] == '(': + close = match_parens(s, after) + idx = s[after + 1:close - 1].strip() + after = close + target = "%s(%s)" % (ident, idx) if idx is not None else ident + args = "%s, " % idx if idx is not None else "" + self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) + self.changed = True + return after + self.emit(s[i:i + 2]) + return i + 2 + + def handle_ptr(self, base, start, end): + """'_ptr' for a converted pin. Only '*name_ptr' dereferences + can be converted automatically; anything else needs manual work.""" + s = self.s + n = len(s) + deref = self.last_sig == '*' + if not deref: + self.rep.warn("'%s_ptr' used as a value; with the new API it is an " + "opaque reference, not a pointer - convert manually" + % base, self.line(start)) + self.emit(s[start:end]) + return end + k = skip_ws_comments(s, end) + idx = None + after = k + decl = self.pinconv[base] + if decl.is_array and k < n and s[k] == '(': + close = match_parens(s, k) + idx = s[k + 1:close - 1].strip() + after = skip_ws_comments(s, close) + op = read_assign_op(s, after) + if op and decl.writable: + opstr, opend = op + if idx is not None: + idx = self.rewrite_index(base, idx, self.line(start)) + if idx is None: + self.emit(s[start:opend]) + return opend + if opstr != '=' and not SIMPLE_IDX_RE.match(idx): + self.rep.warn("compound assignment through '%s_ptr' has an " + "index with side effects; left as-is - " + "convert manually" % base, self.line(start)) + self.emit(s[start:opend]) + return opend + expr, exprend = read_expr(s, skip_ws_comments(s, opend)) + if not expr: + self.rep.warn("could not parse expression in assignment through " + "'%s_ptr'; left as-is" % base, self.line(start)) + self.emit(s[start:opend]) + return opend + if not self.pop_star(): + self.rep.warn("cannot rewrite dereference of '%s_ptr' here; " + "left as-is" % base, self.line(start)) + self.emit(s[start:opend]) + return opend + expr = self.rewrite_expr(expr) + self.emit_gap(end, k) + target = "%s(%s)" % (base, idx) if idx is not None else base + args = "%s, " % idx if idx is not None else "" + if opstr == '=': + new = "%s_set(%s%s)" % (base, args, expr) + else: + new = "%s_set(%s%s %s (%s))" % (base, args, target, + opstr[0], expr) + self.emit(new) + self.changed = True + return exprend + if op: + self.rep.warn("write through '%s_ptr' of read-only pin; left as-is" + % base, self.line(start)) + self.emit(s[start:end]) + self.emit_raw(s[end:after]) + return after + # postfix ++/-- directly after the dereference increments the old + # pointer itself; parenthesized derefs followed by an operator are + # ambiguous - neither can be converted automatically + if s.startswith('++', after) or s.startswith('--', after): + self.rep.warn("postfix %s on '%s_ptr' cannot be converted " + "automatically; left as-is - convert manually" + % (s[after:after + 2], base), self.line(start)) + self.emit(s[start:after + 2]) + return after + 2 + if after < n and s[after] == ')': + k2 = skip_ws_comments(s, after + 1) + op2 = read_assign_op(s, k2) + if op2 or s.startswith('++', k2) or s.startswith('--', k2): + self.rep.warn("parenthesized dereference of '%s_ptr' followed " + "by assignment or increment/decrement; left " + "as-is - convert manually" % base, self.line(start)) + if op2: + pend = op2[1] + else: + pend = k2 + 2 + self.emit(s[start:pend]) + return pend + # dereference read: *name_ptr -> (name), *name_ptr(i) -> (name(i)) + if idx is not None: + idx = self.rewrite_index(base, idx, self.line(start)) + if idx is None: + self.emit(s[start:after]) + return after + if not self.pop_star(): + self.rep.warn("cannot rewrite dereference of '%s_ptr' here; " + "left as-is" % base, self.line(start)) + self.emit(s[start:end]) + self.emit_raw(s[end:after]) + return after + target = "%s(%s)" % (base, idx) if idx is not None else base + self.emit("(%s)" % target) + self.emit_raw(s[end:after]) + self.changed = True + return after + + # -- checks -------------------------------------------------------------- + + def handle_define(self, line, dm, pos): + """Rewrite the body of a #define directive. The directive head + (#define NAME(params)) is preserved; macro parameters shadow + same-named pins/params inside the body.""" + params = [] + if dm.group(2): + params = [p.strip() for p in dm.group(3).split(',') if p.strip()] + head = line[:dm.end()] + code = line[dm.end():] + if not code.strip(): + self.emit_raw(line) + return + for name in list(self.writable) + list(self.readonly): + if re.search(r'\b%s\b' % re.escape(name), head): + self.rep.warn("macro '%s' shadows or redefines pin/param '%s'; " + "not modified - check manually" + % (dm.group(1), name), self.line(pos)) + self.emit_raw(line) + return + sub = BodyRewriter.for_fragment(self, code, exclude=params, + line_offset=self.line(pos) - 1) + rewritten = sub.run() + self.changed = self.changed or sub.changed + self.emit_raw(head) + self.emit(rewritten) + + def check_legacy_refs(self): + for t in LEGACY_C_TYPES: + if re.search(r'\b%s\b' % t, self.s): + self.rep.warn("uses legacy HAL C type '%s'; it is deprecated and " + "will be removed at the HAL API break" % t) + for t in LEGACY_TYPE_MACROS: + if re.search(r'\b%s\b' % t, self.s): + self.rep.warn("uses legacy HAL type constant '%s'; it will be " + "removed at the HAL API break" % t) + + +# --------------------------------------------------------------------------- +# Driver +# --------------------------------------------------------------------------- + +def split_comp(text, rep): + m = re.search(r'\n;;\n', text) + if not m: + rep.error("not a .comp file (no ';;' separator found)") + return text[:m.start()], text[m.end():] + + +def convert(text, filename, quiet=False, c_types=True): + rep = Reporter(filename, quiet) + header, body = split_comp(text, rep) + legacy_api = bool(LEGACY_API_RE.search(body)) + if legacy_api: + rep.warn("uses the legacy hal_pin_*_new/hal_param_*_new creation API " + "directly; the calls and the data they export must be " + "converted to hal_pin_new_/hal_param_new_ " + "manually") + newheader, hchanged, decls, _ = convert_header(header, rep, c_types, + legacy_api) + rewriter = BodyRewriter(body, decls, rep, c_types, legacy_api) + newbody = rewriter.run() + return newheader + "\n;;\n" + newbody, (hchanged or rewriter.changed) + + +def make_backup(fname): + """Create a backup of fname next to it, refusing to follow symlinks or + to clobber an existing backup. Returns the backup path.""" + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0) + for n in range(100): + cand = fname + '.bak' if n == 0 else "%s.bak.%d" % (fname, n) + try: + fd = os.open(cand, flags, 0o644) + except FileExistsError: + continue + with os.fdopen(fd, 'w', newline='') as f: + with open(fname, 'r', newline='') as src: + f.write(src.read()) + return cand + raise SystemExit("halcompupdate: cannot create backup for %s " + "(too many .bak files)" % fname) + + +def write_atomic(fname, text): + """Write text to fname atomically (temp file + rename), preserving the + original file mode.""" + st = os.stat(fname) + fd, tmp = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(fname)), + prefix='.halcompupdate-', suffix='.tmp') + try: + with os.fdopen(fd, 'w', newline='') as f: + f.write(text) + f.flush() + os.fsync(f.fileno()) + os.chmod(tmp, st.st_mode & 0o7777) + os.replace(tmp, fname) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise + + +def main(argv=None): + p = argparse.ArgumentParser( + prog='halcompupdate', + description="Migrate HAL .comp files to the new HAL pin/param API " + "(real/bool/sint/uint/si32/ui32 types and _set() " + "accessors). Without options a unified diff is printed.") + p.add_argument('--in-place', '-i', action='store_true', + help="rewrite files in place (a .bak backup is kept unless " + "--no-backup is given)") + p.add_argument('--no-backup', action='store_true', + help="with --in-place, do not keep a .bak backup") + p.add_argument('--check', action='store_true', + help="do not write anything; exit 1 if a file would change") + p.add_argument('--no-c-types', action='store_true', + help="only convert pin/param declarations and accesses, do " + "not modernize C types (double -> rtapi_real, " + "hal_float_t -> rtapi_real, ...)") + p.add_argument('--quiet', '-q', action='store_true', + help="suppress warnings on stderr") + p.add_argument('files', nargs='+', metavar='file.comp') + args = p.parse_args(argv) + + errors = 0 + changed_any = False + for fname in args.files: + try: + with open(fname, 'r', newline='') as f: + text = f.read() + except OSError as e: + print("halcompupdate: %s" % e, file=sys.stderr) + errors = 1 + continue + newtext, changed = convert(text, fname, args.quiet, + not args.no_c_types) + if not changed: + continue + changed_any = True + if args.check: + continue + if args.in_place: + bak = None + if not args.no_backup: + bak = make_backup(fname) + write_atomic(fname, newtext) + if not args.quiet: + print("halcompupdate: updated %s%s" % + (fname, "" if bak is None else " (backup: %s)" % bak), + file=sys.stderr) + else: + sys.stdout.writelines(difflib.unified_diff( + text.splitlines(True), newtext.splitlines(True), + fromfile=fname, tofile=fname + ".new")) + if errors: + return 2 + return 1 if (args.check and changed_any) else 0 + + +if __name__ == '__main__': + sys.exit(main()) + +# vim:sw=4:sts=4:et:syn=python diff --git a/tests/halcompile/update-api/expected b/tests/halcompile/update-api/expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/halcompile/update-api/test.sh b/tests/halcompile/update-api/test.sh new file mode 100644 index 00000000000..a7fce69368e --- /dev/null +++ b/tests/halcompile/update-api/test.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# Test halcompupdate migration of an old-style component to the new HAL API +set -e + +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT +cp test_update_api.comp "$workdir/" +cd "$workdir" + +# dry-run must report changes via a diff, and not modify the file +if ! halcompupdate test_update_api.comp | grep -q '^+pin out real out0'; then + echo "halcompupdate dry-run did not produce the expected diff" + exit 1 +fi +if grep -q "pin out real out0" test_update_api.comp; then + echo "dry-run modified the input file" + exit 1 +fi + +# --check must exit nonzero when conversion is needed +if halcompupdate --check test_update_api.comp 2>/dev/null; then + echo "--check did not detect a convertible file" + exit 1 +fi + +# convert in place +halcompupdate -i --no-backup test_update_api.comp 2>/dev/null + +# declarations must be converted +for pat in "pin in real in0" "pin out real out0" "pin out bool flag" \ + "pin io si32 count" "pin out si32 result" "param rw real gain" \ + "param rw bool enable"; do + if ! grep -q "$pat" test_update_api.comp; then + echo "converted declaration missing: $pat" + exit 1 + fi +done + +# body writes must use setters (including compound assignments, increments, +# array pins and writes inside #define macros) +for pat in "out0_set(tmp)" "flag_set(tmp > 1.0)" \ + "count_set(count + 1)" "count_set(count + (1))" \ + "result_set(i, steps(i) \* 2)" \ + "flag_set(1)"; do + if ! grep -q "$pat" test_update_api.comp; then + echo "converted body missing: $pat" + exit 1 + fi +done + +# the converted component must compile +if ! halcompile --compile test_update_api.comp >compile.log 2>&1; then + echo "converted component does not compile" + cat compile.log + exit 1 +fi + +# a second run must be a no-op +if ! halcompupdate --check test_update_api.comp 2>/dev/null; then + echo "halcompupdate output is not stable on a second run" + exit 1 +fi + +# constructs that cannot be converted safely must be left unchanged and +# produce a warning +cat > tricky.comp <<'EOF' +component tricky; +pin out float out0; +pin io s32 count; +pin out s32 result-##[8]; +param rw float gain; +function _; +license "GPL"; +;; +extern void helper(double*); +FUNCTION(_) { + int old = count++; + int i = 0; + result(i++) += 2; + (*out0_ptr)++; + helper(&gain); +} +EOF +halcompupdate -i --no-backup tricky.comp 2>warnings.txt +for pat in "int old = count++;" "result(i++) += 2;" "(\*out0_ptr)++;" "helper(&gain);"; do + if ! grep -q "$pat" tricky.comp; then + echo "unsafe construct was modified: $pat" + exit 1 + fi +done +for pat in "postfix ++" "index with side effects" "parenthesized dereference" "address of pin/param"; do + if ! grep -q "$pat" warnings.txt; then + echo "expected warning missing: $pat" + exit 1 + fi +done + +exit 0 + diff --git a/tests/halcompile/update-api/test_update_api.comp b/tests/halcompile/update-api/test_update_api.comp new file mode 100644 index 00000000000..1a8b4875120 --- /dev/null +++ b/tests/halcompile/update-api/test_update_api.comp @@ -0,0 +1,32 @@ +component test_update_api "test component for halcompupdate"; +pin in float in0 "an input"; +pin out float out0 "an output"; +pin out bit flag "a flag"; +pin io s32 count "a counter"; +pin in u32 steps-##[8] "array input"; +pin out s32 result-##[8] "array output"; +param rw float gain = 1.0; +param rw bit enable = 1; +function _; +license "GPL"; +;; +#define CLAMP_IT(v) \ + do { \ + if ((v) > 100) flag = 1; \ + } while (0) + +FUNCTION(_) { + double tmp = in0 * gain; + int i; + if (enable) { + out0 = tmp; + flag = tmp > 1.0; + count = count + 1; + count += 1; + count++; + for (i = 0; i < 8; i++) { + result(i) = steps(i) * 2; + } + CLAMP_IT(tmp); + } +} From a2108134b217e1b87e07801be5f9118596fe567e Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:19:51 +0800 Subject: [PATCH 2/5] halcompupdate: Warn on pin/param writes in EXTRA_SETUP Writes to pins/params inside EXTRA_SETUP convert cleanly to setters, but the setter uses a reference that halcompile initializes only after extra_setup() has run, so the converted component crashes when loaded (seen with multiswitch: top_position_set() on a NULL ref in extra_setup, #4272). Track the current body section while rewriting and warn when a converted write lands in EXTRA_SETUP, distinguishing params (direct assignment worked before, this is a regression) from pins (direct writes were already invalid). EXTRA_CLEANUP and FUNCTION writes stay silent; the conversion itself is unchanged. --- src/hal/utils/halcompupdate.py | 29 +++++++++++++++++++++ tests/halcompile/update-api/test.sh | 39 ++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/hal/utils/halcompupdate.py b/src/hal/utils/halcompupdate.py index ab978b8176e..98a54b6bc21 100644 --- a/src/hal/utils/halcompupdate.py +++ b/src/hal/utils/halcompupdate.py @@ -442,6 +442,7 @@ def __init__(self, body, decls, rep, c_types=True, legacy_api=False): self.last_ident = None # last identifier copied to output self.fragment = False # True when rewriting a sub-expression self.line_offset = 0 + self.section = None # EXTRA_SETUP / EXTRA_CLEANUP / FUNCTION @classmethod def for_fragment(cls, parent, text, exclude=(), line_offset=0): @@ -468,6 +469,7 @@ def for_fragment(cls, parent, text, exclude=(), line_offset=0): sub.last_ident = None sub.fragment = True sub.line_offset = line_offset + sub.section = parent.section return sub def rewrite_expr(self, expr): @@ -590,6 +592,9 @@ def run(self): self.last_ident = ident i = m.end() continue + if not self.fragment and ident in ('EXTRA_SETUP', + 'EXTRA_CLEANUP', 'FUNCTION'): + self.section = ident if self.c_types and ident in self.ctype_map: # keep 'long double' intact if ident == 'double' and self.last_ident == 'long': @@ -732,6 +737,7 @@ def handle_writable(self, ident, start, end): else: new = "%s_set(%s%s %s (%s))" % (ident, args, target, opstr[0], expr) + self.warn_setup_write(ident, decl, start) self.emit(new) self.changed = True return exprend @@ -759,6 +765,7 @@ def handle_writable(self, ident, start, end): self.emit_gap(a, b) target = "%s(%s)" % (ident, idx) if idx is not None else ident args = "%s, " % idx if idx is not None else "" + self.warn_setup_write(ident, decl, start) self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) self.changed = True return after + 2 @@ -783,6 +790,7 @@ def handle_prefix_incdec(self, i): after = close target = "%s(%s)" % (ident, idx) if idx is not None else ident args = "%s, " % idx if idx is not None else "" + self.warn_setup_write(ident, decl, i) self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) self.changed = True return after @@ -843,6 +851,7 @@ def handle_ptr(self, base, start, end): else: new = "%s_set(%s%s %s (%s))" % (base, args, target, opstr[0], expr) + self.warn_setup_write(base, decl, start) self.emit(new) self.changed = True return exprend @@ -894,6 +903,26 @@ def handle_ptr(self, base, start, end): # -- checks -------------------------------------------------------------- + def warn_setup_write(self, ident, decl, pos): + """A pin/param write inside EXTRA_SETUP becomes a setter call on a + reference that halcompile only initializes after extra_setup() has + run, so the converted component crashes when loaded.""" + if self.section != 'EXTRA_SETUP': + return + if decl.kind == 'param': + self.rep.warn("param '%s' is written in EXTRA_SETUP; the setter " + "uses a reference that halcompile initializes only " + "after extra_setup() runs, so the component will " + "crash at load - move the write to the first " + "FUNCTION pass" % ident, self.line(pos)) + else: + self.rep.warn("pin '%s' is written in EXTRA_SETUP; the setter " + "uses a reference that halcompile initializes only " + "after extra_setup() runs, so the component will " + "crash at load (direct pin writes here were already " + "invalid) - move the write to the first FUNCTION " + "pass" % ident, self.line(pos)) + def handle_define(self, line, dm, pos): """Rewrite the body of a #define directive. The directive head (#define NAME(params)) is preserved; macro parameters shadow diff --git a/tests/halcompile/update-api/test.sh b/tests/halcompile/update-api/test.sh index a7fce69368e..bb14aa026f7 100644 --- a/tests/halcompile/update-api/test.sh +++ b/tests/halcompile/update-api/test.sh @@ -95,5 +95,42 @@ for pat in "postfix ++" "index with side effects" "parenthesized dereference" "a fi done -exit 0 +# writes to pins/params in EXTRA_SETUP become setters on references that +# halcompile initializes only after extra_setup() runs, so the converted +# component would crash at load: the conversion must still happen and a +# warning must be issued. Writes in EXTRA_CLEANUP and FUNCTION must not warn. +cat > extrasetup.comp <<'EOF' +component extrasetup; +pin out bit flag; +param rw s32 level; +function _; +license "GPL"; +;; +EXTRA_SETUP(){ + flag = 1; + level = 5; +} +EXTRA_CLEANUP(){ + level = 0; +} +FUNCTION(_) { + level = 0; +} +EOF +halcompupdate -i --no-backup extrasetup.comp 2>setup-warnings.txt +if ! grep -q "flag_set(1)" extrasetup.comp || ! grep -q "level_set(5)" extrasetup.comp; then + echo "EXTRA_SETUP writes were not converted" + exit 1 +fi +for pat in "pin 'flag' is written in EXTRA_SETUP" "param 'level' is written in EXTRA_SETUP"; do + if ! grep -q "$pat" setup-warnings.txt; then + echo "expected warning missing: $pat" + exit 1 + fi +done +if [ "$(grep -c "is written in EXTRA_SETUP" setup-warnings.txt)" != "2" ]; then + echo "EXTRA_CLEANUP or FUNCTION write triggered a spurious warning" + exit 1 +fi +exit 0 From e66bcd8877cf0bd267f7cf84264093ab4856978d Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:05:38 +0800 Subject: [PATCH 3/5] halcompupdate: Point EXTRA_SETUP write warnings at POST_EXPORT POST_EXPORT() (halcompile option post_export yes) is now the supported place for pin/param initialization that must happen after creation, so advise it instead of the first FUNCTION pass. Assert the advice in the test. --- src/hal/utils/halcompupdate.py | 8 ++++---- tests/halcompile/update-api/test.sh | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/hal/utils/halcompupdate.py b/src/hal/utils/halcompupdate.py index 98a54b6bc21..88b94aab5f2 100644 --- a/src/hal/utils/halcompupdate.py +++ b/src/hal/utils/halcompupdate.py @@ -913,15 +913,15 @@ def warn_setup_write(self, ident, decl, pos): self.rep.warn("param '%s' is written in EXTRA_SETUP; the setter " "uses a reference that halcompile initializes only " "after extra_setup() runs, so the component will " - "crash at load - move the write to the first " - "FUNCTION pass" % ident, self.line(pos)) + "crash at load - move the write to POST_EXPORT() " + "(option post_export yes)" % ident, self.line(pos)) else: self.rep.warn("pin '%s' is written in EXTRA_SETUP; the setter " "uses a reference that halcompile initializes only " "after extra_setup() runs, so the component will " "crash at load (direct pin writes here were already " - "invalid) - move the write to the first FUNCTION " - "pass" % ident, self.line(pos)) + "invalid) - move the write to POST_EXPORT() " + "(option post_export yes)" % ident, self.line(pos)) def handle_define(self, line, dm, pos): """Rewrite the body of a #define directive. The directive head diff --git a/tests/halcompile/update-api/test.sh b/tests/halcompile/update-api/test.sh index bb14aa026f7..dd2c43460c0 100644 --- a/tests/halcompile/update-api/test.sh +++ b/tests/halcompile/update-api/test.sh @@ -128,6 +128,10 @@ for pat in "pin 'flag' is written in EXTRA_SETUP" "param 'level' is written in E exit 1 fi done +if [ "$(grep -c "POST_EXPORT()" setup-warnings.txt)" != "2" ]; then + echo "warnings should point at POST_EXPORT()" + exit 1 +fi if [ "$(grep -c "is written in EXTRA_SETUP" setup-warnings.txt)" != "2" ]; then echo "EXTRA_CLEANUP or FUNCTION write triggered a spurious warning" exit 1 From 8e367632d78011a057a8baaeb1a99929231f5dbe Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:40:09 +0800 Subject: [PATCH 4/5] halcompupdate: Preserve volatile for legacy hal_*_t types, print summary Body code converts legacy hal_*_t types to the volatile-preserving form ('hal_bit_t x' -> 'volatile rtapi_bool x'), semantics-identical whether or not the qualifier is load-bearing; each such conversion is reported with its line number so the spots can be reviewed. 'variable' declarations cannot express volatile, so legacy hal_*_t types there are reported and left unchanged for manual conversion. Body warnings now report file line numbers instead of body-relative ones. Each file ends with a summary of mechanical changes applied and constructs left for manual review. --- docs/src/man/man1/halcompupdate.1.adoc | 23 +++-- src/hal/utils/halcompupdate.py | 124 +++++++++++++++---------- tests/halcompile/update-api/test.sh | 57 ++++++++++++ 3 files changed, 149 insertions(+), 55 deletions(-) diff --git a/docs/src/man/man1/halcompupdate.1.adoc b/docs/src/man/man1/halcompupdate.1.adoc index 25d5ec28c34..fa16f169d67 100644 --- a/docs/src/man/man1/halcompupdate.1.adoc +++ b/docs/src/man/man1/halcompupdate.1.adoc @@ -33,14 +33,17 @@ The following transformations are applied: * Dereferences of the old pin pointer macro are rewritten: ***name_ptr*** reads become *(name)* and ***name_ptr = x*** writes become *name_set(x)*. -* Legacy C types are modernized with their variable-use replacements - (*double* -> *rtapi_real*, *hal_float_t* -> *rtapi_real*, - *hal_bit_t* -> *rtapi_bool*, and so on). The *volatile* qualifier of - the legacy types is dropped on purpose: it existed for direct access - to HAL memory and was never right for variables. Pointers to the - legacy types referenced HAL memory and become opaque references in the - new API; those are warned about and left unchanged. Use - *--no-c-types* to skip this conversion. +* Legacy C types are modernized (*double* -> *rtapi_real* and so on). + The legacy *hal_*_t* types carry a *volatile* qualifier, which is + preserved in body code (*hal_bit_t x* becomes *volatile rtapi_bool x*), + so the conversion is semantics-identical whether or not the code relies + on the volatility. Each such conversion is reported with its line + number so the spots can be reviewed and the qualifier dropped by hand + where it is not needed. In *variable* declarations the qualifier cannot be + expressed, so *hal_*_t* types there are reported and left unchanged for + manual conversion. Pointers to the legacy types referenced HAL memory + and become opaque references in the new API; those are warned about and + left unchanged. Use *--no-c-types* to skip the C type conversion. Reading pins and params is unchanged: the bare name (or *name(idx)* for arrays) evaluates to the value in both APIs. @@ -53,6 +56,10 @@ functions directly, or that pass the address of a pin (*&name*) to helper functions, cannot be converted automatically. *halcompupdate* prints a warning for those cases so they can be converted by hand. +After each file a summary line reports how many mechanical changes were +applied and how many constructs were left for manual review. Always +review the diff and test the converted component before use. + == OPTIONS *compfile...*:: diff --git a/src/hal/utils/halcompupdate.py b/src/hal/utils/halcompupdate.py index 88b94aab5f2..6a0e468cbd7 100644 --- a/src/hal/utils/halcompupdate.py +++ b/src/hal/utils/halcompupdate.py @@ -51,18 +51,18 @@ 'unsigned': 'ui32', } -# C types used with the old HAL API and their modern replacements for -# variable use. The volatile qualifier of the legacy hal_*_t types exists -# only because they were used for direct access to HAL memory; as variable -# types they were always wrong, so the plain rtapi_* forms are used here. -# Plain C 'float' is deliberately not mapped: it is not part of the HAL -# API and stays valid C (only 'variable' declarations are converted). -# Pointers to the legacy types are a different matter (they referenced -# HAL memory and become opaque hal_*_t references); those are warned -# about and left unchanged. +# Plain C types carry no volatile qualifier, modernizing them is always +# safe. Plain C 'float' is not part of the HAL API and stays valid C +# (only 'variable' declarations are converted). CTYPE_MAP = { 'double': 'rtapi_real', 'real_t': 'rtapi_real', +} + +# The legacy hal_*_t types are volatile. The conversion keeps the +# qualifier ('hal_bit_t x' -> 'volatile rtapi_bool x'), which is +# semantics-identical. Pointers are warned about and left unchanged. +CTYPE_HAL_MAP = { 'hal_float_t': 'rtapi_real', 'hal_bit_t': 'rtapi_bool', 'hal_s32_t': 'rtapi_s32', @@ -71,29 +71,23 @@ 'hal_u64_t': 'rtapi_u64', } -# Mapping for 'variable' declarations. The comp grammar allows only a -# single-word type there, and instance variables are component-private -# memory, so the plain rtapi_* forms are used (no volatile qualifier). -# Old code frequently used HAL type names here ('float' for HAL floats); -# C 'float' variables holding HAL values must become rtapi_real. -# 'signed'/'unsigned' are *not* mapped: they are legitimate plain C types. +# Mapping for 'variable' declarations; 'float' for HAL floats must become +# rtapi_real. 'signed'/'unsigned' are legitimate plain C types, not +# mapped. The hal_*_t types are not mapped either: the grammar allows +# only a single-word type here, so volatile cannot be preserved; they are +# reported and left unchanged, convert them by hand. VAR_TYPE_MAP = { 'double': 'rtapi_real', 'real_t': 'rtapi_real', 'float': 'rtapi_real', - 'hal_float_t': 'rtapi_real', - 'hal_bit_t': 'rtapi_bool', - 'hal_s32_t': 'rtapi_s32', - 'hal_u32_t': 'rtapi_u32', - 'hal_s64_t': 'rtapi_s64', - 'hal_u64_t': 'rtapi_u64', } -# HAL C types/macros that will disappear at the API break; used in bodies +# HAL C types/macros that disappear at the API break and are not +# converted; used in bodies. The convertible hal_*_t types +# (CTYPE_HAL_MAP) are not listed: they are rewritten or get a specific +# warning (pointers, 'variable' declarations). LEGACY_C_TYPES = [ - 'hal_bit_t', 'hal_float_t', 'hal_s32_t', 'hal_u32_t', - 'hal_s64_t', 'hal_u64_t', 'hal_data_u', 'hal_pin_dir_t', - 'hal_param_dir_t', + 'hal_data_u', 'hal_pin_dir_t', 'hal_param_dir_t', ] LEGACY_TYPE_MACROS = [ 'HAL_BIT', 'HAL_FLOAT', 'HAL_S32', 'HAL_U32', 'HAL_S64', 'HAL_U64', @@ -114,8 +108,11 @@ class Reporter: def __init__(self, filename, quiet=False): self.filename = filename self.quiet = quiet + self.warnings = 0 # constructs left for manual review + self.edits = 0 # mechanical changes applied def warn(self, msg, lineno=0): + self.warnings += 1 if not self.quiet: print("%s:%d: Warning: %s" % (self.filename, lineno or 0, msg), file=sys.stderr) @@ -198,7 +195,8 @@ def split_statements(header): r'^(\s*(?:(?://[^\n]*|/\*.*?\*/)\s*)*)' # leading whitespace/comments r'variable\s+' r'([A-Za-z_][A-Za-z0-9_]*)\s+' # C type - r'\*?\s*[#A-Za-z_]', # (possibly starred) name + r'(\*?\s*)' # optional pointer star + r'([#A-Za-z_][-#A-Za-z0-9_.]*)', # variable name re.S) @@ -244,11 +242,6 @@ def convert_header(header, rep, c_types=True, legacy_api=False): if 'no_convenience_defines' in options: rep.error("component uses 'option no_convenience_defines'; " "automatic body conversion is not possible") - var_map = VAR_TYPE_MAP - if legacy_api: - # keep hal_*_t: their addresses are passed to legacy API calls - var_map = {k: v for k, v in VAR_TYPE_MAP.items() - if not k.startswith('hal_')} edits = [] # (start, end, replacement) in header coordinates for start, end in split_statements(header): stmt = header[start:end] @@ -261,11 +254,24 @@ def convert_header(header, rep, c_types=True, legacy_api=False): continue if c_types: mv = VAR_RE.match(stmt) - if mv and mv.group(2) in var_map: - edits.append((start + mv.start(2), start + mv.end(2), - var_map[mv.group(2)])) + if mv: + vtype, vname = mv.group(2), mv.group(4) + if vtype in VAR_TYPE_MAP: + edits.append((start + mv.start(2), start + mv.end(2), + VAR_TYPE_MAP[vtype])) + elif vtype in CTYPE_HAL_MAP: + if not legacy_api: + rep.warn("variable '%s' uses legacy HAL type '%s'; " + "the volatile qualifier may be load-bearing " + "and cannot be preserved here - left " + "unchanged, convert to '%s' by hand" + % (vname, vtype, CTYPE_HAL_MAP[vtype]), + lineno_of(header, start)) + # legacy_api: addresses of these are passed to legacy + # API calls; the legacy API warning already covers it if not edits: return header, False, decls, options + rep.edits += len(edits) out = [] prev = 0 for s0, e0, repl in edits: @@ -414,7 +420,8 @@ def read_expr(s, i): class BodyRewriter: - def __init__(self, body, decls, rep, c_types=True, legacy_api=False): + def __init__(self, body, decls, rep, c_types=True, legacy_api=False, + line_offset=0): self.s = body self.rep = rep self.c_types = c_types @@ -424,7 +431,10 @@ def __init__(self, body, decls, rep, c_types=True, legacy_api=False): if legacy_api: self.ctype_map = {'double': 'rtapi_real'} else: - self.ctype_map = CTYPE_MAP + # preserve the volatile qualifier: semantics-identical + self.ctype_map = dict(CTYPE_MAP) + for k, v in CTYPE_HAL_MAP.items(): + self.ctype_map[k] = 'volatile ' + v self.out = [] # Only converted (old-type) decls need rewriting; new-type decls # are already fine and 'port' keeps direct access. @@ -441,7 +451,7 @@ def __init__(self, body, decls, rep, c_types=True, legacy_api=False): self.last_sig2 = '' # the one before it self.last_ident = None # last identifier copied to output self.fragment = False # True when rewriting a sub-expression - self.line_offset = 0 + self.line_offset = line_offset # file line number of body line 1, - 1 self.section = None # EXTRA_SETUP / EXTRA_CLEANUP / FUNCTION @classmethod @@ -480,6 +490,11 @@ def rewrite_expr(self, expr): self.changed = self.changed or sub.changed return out + def note_change(self): + """Record one mechanical rewrite.""" + self.changed = True + self.rep.edits += 1 + def emit(self, text): """Emit significant code, tracking the last tokens seen.""" self.out.append(text) @@ -607,8 +622,15 @@ def run(self): self.line(i)) self.emit(ident) else: - self.emit(self.ctype_map[ident]) - self.changed = True + repl = self.ctype_map[ident] + if repl.startswith('volatile '): + self.rep.warn("'%s' converted to '%s'; the " + "qualifier is kept because the tool " + "cannot know if it is needed - drop " + "it by hand if not" + % (ident, repl), self.line(i)) + self.emit(repl) + self.note_change() self.last_ident = ident i = m.end() continue @@ -739,7 +761,7 @@ def handle_writable(self, ident, start, end): opstr[0], expr) self.warn_setup_write(ident, decl, start) self.emit(new) - self.changed = True + self.note_change() return exprend if s.startswith('++', after) or s.startswith('--', after): sign = '+' if s.startswith('++', after) else '-' @@ -767,7 +789,7 @@ def handle_writable(self, ident, start, end): args = "%s, " % idx if idx is not None else "" self.warn_setup_write(ident, decl, start) self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) - self.changed = True + self.note_change() return after + 2 # plain read access: unchanged self.emit(s[start:end]) @@ -792,7 +814,7 @@ def handle_prefix_incdec(self, i): args = "%s, " % idx if idx is not None else "" self.warn_setup_write(ident, decl, i) self.emit("%s_set(%s%s %s 1)" % (ident, args, target, sign)) - self.changed = True + self.note_change() return after self.emit(s[i:i + 2]) return i + 2 @@ -853,7 +875,7 @@ def handle_ptr(self, base, start, end): opstr[0], expr) self.warn_setup_write(base, decl, start) self.emit(new) - self.changed = True + self.note_change() return exprend if op: self.rep.warn("write through '%s_ptr' of read-only pin; left as-is" @@ -898,7 +920,7 @@ def handle_ptr(self, base, start, end): target = "%s(%s)" % (base, idx) if idx is not None else base self.emit("(%s)" % target) self.emit_raw(s[end:after]) - self.changed = True + self.note_change() return after # -- checks -------------------------------------------------------------- @@ -982,9 +1004,17 @@ def convert(text, filename, quiet=False, c_types=True): "manually") newheader, hchanged, decls, _ = convert_header(header, rep, c_types, legacy_api) - rewriter = BodyRewriter(body, decls, rep, c_types, legacy_api) + body_line = lineno_of(text, len(header) + len("\n;;\n")) - 1 + rewriter = BodyRewriter(body, decls, rep, c_types, legacy_api, + line_offset=body_line) newbody = rewriter.run() - return newheader + "\n;;\n" + newbody, (hchanged or rewriter.changed) + changed = hchanged or rewriter.changed + if not quiet and (changed or rep.warnings): + print("halcompupdate: %s: %d mechanical change(s), %d construct(s) " + "left for manual review - review the diff and test the " + "component before use" + % (filename, rep.edits, rep.warnings), file=sys.stderr) + return newheader + "\n;;\n" + newbody, changed def make_backup(fname): @@ -1042,7 +1072,7 @@ def main(argv=None): p.add_argument('--no-c-types', action='store_true', help="only convert pin/param declarations and accesses, do " "not modernize C types (double -> rtapi_real, " - "hal_float_t -> rtapi_real, ...)") + "hal_float_t -> volatile rtapi_real, ...)") p.add_argument('--quiet', '-q', action='store_true', help="suppress warnings on stderr") p.add_argument('files', nargs='+', metavar='file.comp') diff --git a/tests/halcompile/update-api/test.sh b/tests/halcompile/update-api/test.sh index dd2c43460c0..10d9495e3dc 100644 --- a/tests/halcompile/update-api/test.sh +++ b/tests/halcompile/update-api/test.sh @@ -137,4 +137,61 @@ if [ "$(grep -c "is written in EXTRA_SETUP" setup-warnings.txt)" != "2" ]; then exit 1 fi +# legacy hal_*_t types: in bodies the volatile qualifier is preserved +# (semantics-identical); in 'variable' declarations it cannot be +# expressed, so the declaration is left unchanged with a warning. +cat > voltest.comp <<'EOF' +component voltest; +pin out float out0; +variable hal_bit_t vflag; +variable float bias; +function _; +license "GPL"; +;; +FUNCTION(_) { + hal_bit_t local = 0; + hal_float_t acc = bias; + out0 = acc; +} +EOF +halcompupdate -i --no-backup voltest.comp 2>vol-warnings.txt +if ! grep -q "volatile rtapi_bool local" voltest.comp; then + echo "body hal_bit_t did not keep its volatile qualifier" + exit 1 +fi +if ! grep -q "volatile rtapi_real acc" voltest.comp; then + echo "body hal_float_t did not keep its volatile qualifier" + exit 1 +fi +if ! grep -q "variable rtapi_real bias" voltest.comp; then + echo "plain float variable was not converted" + exit 1 +fi +if ! grep -q "variable hal_bit_t vflag" voltest.comp; then + echo "variable hal_bit_t was modified; it must be left for manual review" + exit 1 +fi +if ! grep -q "variable 'vflag' uses legacy HAL type 'hal_bit_t'" vol-warnings.txt; then + echo "expected warning missing for variable hal_bit_t" + exit 1 +fi +for pat in "'hal_bit_t' converted to 'volatile rtapi_bool'" \ + "'hal_float_t' converted to 'volatile rtapi_real'"; do + if ! grep -q "$pat" vol-warnings.txt; then + echo "expected volatile-kept warning missing: $pat" + exit 1 + fi +done +if ! grep -q "construct(s) left for manual review" vol-warnings.txt; then + echo "closing summary missing" + exit 1 +fi +# fix the reported variable by hand, then the component must compile +sed -i 's/variable hal_bit_t vflag/variable rtapi_bool vflag/' voltest.comp +if ! halcompile --compile voltest.comp >vol-compile.log 2>&1; then + echo "volatile-converted component does not compile" + cat vol-compile.log + exit 1 +fi + exit 0 From 52f3191022adae5016edfc748d3d8036d9fa14ef Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:43:00 -0400 Subject: [PATCH 5/5] halcompupdate: note names that spell a legacy type A pin, param, component or function name containing a legacy type as a segment (out_s32, input_bit, conv_s32_float) is stale once the type is removed at the API break. Note it and suggest the new-style spelling (out_si32, input_bool, conv_si32_real), leaving the rename to the author: names are part of the HAL configuration interface - the component name is the loadrt/loadusr argument and module name, functions are exported as comp.N.name - so they are never changed, and the note does not affect the --check exit status or the manual-review count. The word in a name may not mean the type (plasmac's float_switch is a hardware float switch, demux's sel-bit counts bits of a value); the note is then simply dismissed. Only full segments match, so 'floating' is not noted for 'float', and duplicated segments are mentioned once. 'signed' and 'unsigned' are not checked in names: no sensible word replaces them there. The note points at the line of the declaration itself, and the per-file summary reports the note count even when nothing else needed conversion. Run over all in-tree components: 54 names are noted (the 34 conv_*, abs_*, scaled_s32_sums and tristate_* component names, and 20 pins in demux, histobins, histobinstream, multiswitch, plasmac, reset, mesa_7i65), each a legacy type word used as a name segment. --- docs/src/hal/comp.adoc | 5 + docs/src/man/man1/halcompupdate.1.adoc | 24 ++++- src/hal/utils/halcompupdate.py | 143 ++++++++++++++++++++++--- tests/halcompile/update-api/test.sh | 129 ++++++++++++++++++++++ 4 files changed, 280 insertions(+), 21 deletions(-) diff --git a/docs/src/hal/comp.adoc b/docs/src/hal/comp.adoc index 943a464eefb..62aadde942c 100644 --- a/docs/src/hal/comp.adoc +++ b/docs/src/hal/comp.adoc @@ -530,6 +530,11 @@ halcompupdate -i mycomponent.comp # rewrite the file in place (keeps a .ba The tool converts the declaration types and rewrites writes to pins and parameters (including compound assignments and dereferences of 'name_ptr') to the '_set' form. It deliberately does not rename pins or parameters, because their names are part of the HAL configuration interface. +It does point out names that still spell a legacy type, though: a pin, parameter, component or +function name that contains 'float', 'bit', 's32', 'u32', 's64' or 'u64' as a name segment (for +example 'out_s32', 'input_bit' or 'conv_s32_float') is noted together with the new-style spelling +(for example 'out_si32' or 'conv_si32_real'); the rename is optional and left to the author, who +decides whether the word really meant the type (a pin named 'sel-bit' counts bits of a value). Components that use the legacy 'hal_pin_*_new'/'hal_param_*_new' creation functions or that pass the address of a pin ('&name') to helper functions cannot be converted automatically; 'halcompupdate' prints a warning for those cases so they can be converted by hand. diff --git a/docs/src/man/man1/halcompupdate.1.adoc b/docs/src/man/man1/halcompupdate.1.adoc index fa16f169d67..113248ed189 100644 --- a/docs/src/man/man1/halcompupdate.1.adoc +++ b/docs/src/man/man1/halcompupdate.1.adoc @@ -49,7 +49,21 @@ Reading pins and params is unchanged: the bare name (or *name(idx)* for arrays) evaluates to the value in both APIs. Pin and parameter names are never changed, because they are part of the -HAL configuration interface used by .hal files. +HAL configuration interface used by .hal files. A name that spells out +a legacy type, however, is pointed out: *halcompupdate* notes every +name - of a pin, a parameter, the component or a function - that +contains a segment naming a type that will be removed at the API break +(*float*, *bit*, *s32*, *u32*, *s64*, *u64*) and suggests the +new-style spelling, for example *out_s32* -> *out_si32* or +*conv_s32_float* -> *conv_si32_real*. The component name is the +loadrt/loadusr argument and the module name, and functions are exported +as +*comp.N.function*, so a renamed component or function name affects +existing configurations as well. The note is optional, does not modify +the file, and does not affect the *--check* exit status; the author +decides whether the word in the name really meant the type (a pin named +*sel-bit* counts bits of a value, a pin named *float_switch* is a +hardware float switch). Components that use the legacy *hal_pin_*_new*/*hal_param_*_new* creation functions directly, or that pass the address of a pin (*&name*) to helper @@ -57,8 +71,10 @@ functions, cannot be converted automatically. *halcompupdate* prints a warning for those cases so they can be converted by hand. After each file a summary line reports how many mechanical changes were -applied and how many constructs were left for manual review. Always -review the diff and test the converted component before use. +applied, how many constructs were left for manual review, and how many +names mention a legacy type (optional rename); if nothing needed to be +converted, it reports the name notes only. Always review the diff and +test the converted component before use. == OPTIONS @@ -82,7 +98,7 @@ Only convert pin/param declarations and accesses; do not modernize C types in the component body. *-q*, *--quiet*:: -Suppress warnings on stderr. +Suppress warnings and notes on stderr. == EXAMPLES diff --git a/src/hal/utils/halcompupdate.py b/src/hal/utils/halcompupdate.py index 6a0e468cbd7..69110fe2f65 100644 --- a/src/hal/utils/halcompupdate.py +++ b/src/hal/utils/halcompupdate.py @@ -32,6 +32,11 @@ # # This tool rewrites a .comp file accordingly. By default it prints a # unified diff; use --in-place to rewrite the file. +# +# It never renames (names are part of the HAL configuration interface), +# but it notes pin, param, component and function names that spell a +# legacy type (e.g. 'out_s32', 'conv_s32_float') and suggests the +# new-style spelling, leaving the rename to the author. import argparse import difflib @@ -51,6 +56,18 @@ 'unsigned': 'ui32', } +# Legacy declaration types that may be spelled out as a segment of a +# pin/param/component/function name ('out_s32', 'input_bit', +# 'conv_s32_float'). Such a word is stale once the type is removed at +# the API break, so it is noted for an optional rename. 'signed' and +# 'unsigned' are not included: there is no sensible word to replace +# them with in a name, and no in-tree name uses them. The suggestion +# is mechanical (OLD2NEW); the author decides whether the word really +# meant the type (a pin named 'sel-bit' counts bits of a value, +# plasmac's 'float_switch' is a hardware float switch) - a dismissable +# note, like all of these. +NAME_LEGACY_TYPES = ('float', 'bit', 's32', 'u32', 's64', 'u64') + # Plain C types carry no volatile qualifier, modernizing them is always # safe. Plain C 'float' is not part of the HAL API and stays valid C # (only 'variable' declarations are converted). @@ -110,6 +127,7 @@ def __init__(self, filename, quiet=False): self.quiet = quiet self.warnings = 0 # constructs left for manual review self.edits = 0 # mechanical changes applied + self.notes = 0 # gentle suggestions (legacy type in name) def warn(self, msg, lineno=0): self.warnings += 1 @@ -117,6 +135,13 @@ def warn(self, msg, lineno=0): print("%s:%d: Warning: %s" % (self.filename, lineno or 0, msg), file=sys.stderr) + def note(self, msg, lineno=0): + """A gentle suggestion; not counted as manual review work.""" + self.notes += 1 + if not self.quiet: + print("%s:%d: Note: %s" % (self.filename, lineno or 0, msg), + file=sys.stderr) + def error(self, msg): raise SystemExit("%s:0: Error: %s" % (self.filename, msg)) @@ -199,15 +224,28 @@ def split_statements(header): r'([#A-Za-z_][-#A-Za-z0-9_.]*)', # variable name re.S) +COMPONENT_RE = re.compile( + r'^(\s*(?:(?://[^\n]*|/\*.*?\*/)\s*)*)' # leading whitespace/comments + r'component\s+' + r'([A-Za-z_][A-Za-z0-9_]*)', # component name + re.S) + +FUNCTION_RE = re.compile( + r'^(\s*(?:(?://[^\n]*|/\*.*?\*/)\s*)*)' # leading whitespace/comments + r'function\s+' + r'([A-Za-z_][A-Za-z0-9_]*)', # function name + re.S) + class Decl: - def __init__(self, kind, direction, type_, name, is_array): + def __init__(self, kind, direction, type_, name, is_array, lineno=0): self.kind = kind # 'pin' or 'param' self.direction = direction self.type = type_ self.name = name # HAL name self.cname = to_c(name) self.is_array = is_array + self.lineno = lineno # line of the declaration statement @property def writable(self): @@ -217,28 +255,64 @@ def writable(self): def parse_declarations(header): - """Find pin/param declarations, return (list of Decl, set of options).""" + """Find named items in the header. Returns (list of Decl, set of + options, list of (kind, name, lineno)) where the list covers pins, + params, the component and the functions, in file order.""" decls = [] options = set() + named = [] for start, end in split_statements(header): stmt = header[start:end] m = DECL_RE.match(stmt) if m: _, kind, direction, type_, name, array = m.groups() - decls.append(Decl(kind, direction, type_, name, bool(array))) + d = Decl(kind, direction, type_, name, bool(array), + lineno_of(header, start + m.start(2))) + decls.append(d) + named.append((kind, name, d.lineno)) + continue + mc = COMPONENT_RE.match(stmt) + if mc: + named.append(('component', mc.group(2), + lineno_of(header, start + mc.start(2)))) + continue + mf = FUNCTION_RE.match(stmt) + if mf: + named.append(('function', mf.group(2), + lineno_of(header, start + mf.start(2)))) continue mo = re.match(r'\s*option\s+([A-Za-z_][A-Za-z0-9_]*)', stmt) if mo: options.add(mo.group(1)) - return decls, options + return decls, options, named + + +def name_rename_suggestion(name): + """If a segment of the name spells a legacy declaration type, + return (newname, [types]) with those segments replaced by their + new-style spelling; otherwise None. Separators are preserved and + duplicated types are listed once.""" + found = [] + parts = [] + for p in re.findall(r'[A-Za-z0-9]+|[^A-Za-z0-9]', name): + t = p.lower() + if t in NAME_LEGACY_TYPES: + if t not in found: + found.append(t) + parts.append(OLD2NEW[t]) + else: + parts.append(p) + if not found: + return None + return ''.join(parts), found def convert_header(header, rep, c_types=True, legacy_api=False): """Replace old HAL types by new ones in pin/param declarations and modernize C types in 'variable' declarations. Only the type token itself is replaced, all other text (spacing, docs) is preserved. - Returns (new_header, changed, decls, options).""" - decls, options = parse_declarations(header) + Returns (new_header, changed, decls, options, named).""" + decls, options, named = parse_declarations(header) if 'no_convenience_defines' in options: rep.error("component uses 'option no_convenience_defines'; " "automatic body conversion is not possible") @@ -266,11 +340,11 @@ def convert_header(header, rep, c_types=True, legacy_api=False): "and cannot be preserved here - left " "unchanged, convert to '%s' by hand" % (vname, vtype, CTYPE_HAL_MAP[vtype]), - lineno_of(header, start)) + lineno_of(header, start + mv.end(1))) # legacy_api: addresses of these are passed to legacy # API calls; the legacy API warning already covers it if not edits: - return header, False, decls, options + return header, False, decls, options, named rep.edits += len(edits) out = [] prev = 0 @@ -279,7 +353,7 @@ def convert_header(header, rep, c_types=True, legacy_api=False): out.append(repl) prev = e0 out.append(header[prev:]) - return ''.join(out), True, decls, options + return ''.join(out), True, decls, options, named # --------------------------------------------------------------------------- @@ -1002,18 +1076,53 @@ def convert(text, filename, quiet=False, c_types=True): "directly; the calls and the data they export must be " "converted to hal_pin_new_/hal_param_new_ " "manually") - newheader, hchanged, decls, _ = convert_header(header, rep, c_types, - legacy_api) + newheader, hchanged, decls, _, named = convert_header(header, rep, + c_types, legacy_api) + # Why a rename would matter, per kind of named item + rename_reason = { + 'pin': "the name is part of the HAL interface, so existing " + ".hal files would need updating", + 'param': "the name is part of the HAL interface, so existing " + ".hal files would need updating", + 'component': "the name is the loadrt/loadusr argument and the " + "module name, so existing configs would need updating", + 'function': "the name is exported as .., so " + "existing .hal files would need updating", + } + for kind, name, lineno in named: + sug = name_rename_suggestion(name) + if sug: + newname, types = sug + if len(types) == 1: + what = "'%s'" % types[0] + else: + what = ', '.join("'%s'" % t for t in types) + rep.note("%s name '%s' mentions legacy HAL type%s %s, which " + "will be removed at the HAL API break; consider " + "renaming to '%s' - %s" + % (kind, name, '' if len(types) == 1 else 's', + what, newname, rename_reason[kind]), lineno) body_line = lineno_of(text, len(header) + len("\n;;\n")) - 1 rewriter = BodyRewriter(body, decls, rep, c_types, legacy_api, line_offset=body_line) newbody = rewriter.run() changed = hchanged or rewriter.changed - if not quiet and (changed or rep.warnings): - print("halcompupdate: %s: %d mechanical change(s), %d construct(s) " - "left for manual review - review the diff and test the " - "component before use" - % (filename, rep.edits, rep.warnings), file=sys.stderr) + if not quiet and (changed or rep.warnings or rep.notes): + if changed or rep.warnings: + summary = ("halcompupdate: %s: %d mechanical change(s), " + "%d construct(s) left for manual review" + % (filename, rep.edits, rep.warnings)) + if rep.notes: + summary += (", %d name(s) mention a legacy HAL type " + "(rename is optional)" % rep.notes) + summary += (" - review the diff and test the component " + "before use") + else: + # nothing to convert; report the optional renames only + summary = ("halcompupdate: %s: %d name(s) mention a legacy " + "HAL type (rename is optional)" + % (filename, rep.notes)) + print(summary, file=sys.stderr) return newheader + "\n;;\n" + newbody, changed @@ -1074,7 +1183,7 @@ def main(argv=None): "not modernize C types (double -> rtapi_real, " "hal_float_t -> volatile rtapi_real, ...)") p.add_argument('--quiet', '-q', action='store_true', - help="suppress warnings on stderr") + help="suppress warnings and notes on stderr") p.add_argument('files', nargs='+', metavar='file.comp') args = p.parse_args(argv) diff --git a/tests/halcompile/update-api/test.sh b/tests/halcompile/update-api/test.sh index 10d9495e3dc..9778343b67b 100644 --- a/tests/halcompile/update-api/test.sh +++ b/tests/halcompile/update-api/test.sh @@ -61,6 +61,17 @@ if ! halcompupdate --check test_update_api.comp 2>/dev/null; then exit 1 fi +# the names in the converted component contain no legacy type segments, +# so a re-run must not emit any rename note +if ! halcompupdate test_update_api.comp >/dev/null 2>rename-notes.txt; then + echo "halcompupdate re-run failed" + exit 1 +fi +if grep -q "mentions legacy HAL type" rename-notes.txt; then + echo "spurious rename note for names without type segments" + exit 1 +fi + # constructs that cannot be converted safely must be left unchanged and # produce a warning cat > tricky.comp <<'EOF' @@ -68,6 +79,9 @@ component tricky; pin out float out0; pin io s32 count; pin out s32 result-##[8]; +pin out s32 out_s32; +pin out bit input_bit; +pin out float floating; param rw float gain; function _; license "GPL"; @@ -95,6 +109,121 @@ for pat in "postfix ++" "index with side effects" "parenthesized dereference" "a fi done +# a name spelling a legacy type (any of them, bit and float included) +# gets a gentle rename note with the new-style spelling suggested. The +# note is not counted as manual-review work and does not change the +# file; the author decides whether the word really meant the type. A +# word merely containing a type (floating) is not a segment and must +# not be noted. The note must point at the line of the declaration, +# not of the statement before it. +if ! grep -q "tricky.comp:5: Note: pin name 'out_s32' mentions legacy HAL type 's32'" warnings.txt; then + echo "expected rename note missing for out_s32 (or wrong line number)" + exit 1 +fi +if ! grep -q "consider renaming to 'out_si32'" warnings.txt; then + echo "expected rename suggestion missing for out_s32" + exit 1 +fi +if ! grep -q "pin name 'input_bit' mentions legacy HAL type 'bit'" warnings.txt; then + echo "expected rename note missing for input_bit" + exit 1 +fi +if ! grep -q "consider renaming to 'input_bool'" warnings.txt; then + echo "expected rename suggestion missing for input_bit" + exit 1 +fi +if grep -q "name 'floating'" warnings.txt; then + echo "spurious rename note for a word containing 'float'" + exit 1 +fi +if ! grep -q "2 name(s) mention a legacy HAL type (rename is optional)" warnings.txt; then + echo "rename note count missing from the summary" + exit 1 +fi +# the note itself must not rename: the types are converted, the names stay +for pat in "pin out si32 out_s32;" "pin out bool input_bit;" "pin out real floating;"; do + if ! grep -q "$pat" tricky.comp; then + echo "rename note must not modify the name: $pat" + exit 1 + fi +done + +# component and function names are HAL-visible too: the component name +# is the loadrt argument and the module name, functions are exported as +# comp.N.name. They are noted the same way, and duplicated type +# segments are mentioned once. +cat > naming.comp <<'EOF' +component conv_s32_float; +pin out float value_s32; +function conv_s32_s32; +license "GPL"; +;; +FUNCTION(conv_s32_s32) { + value_s32 = 1.0; +} +EOF +halcompupdate -i --no-backup naming.comp 2>name-notes.txt +if ! grep -q "naming.comp:1: Note: component name 'conv_s32_float'" name-notes.txt; then + echo "expected component name note missing (or wrong line number)" + exit 1 +fi +if ! grep -q "consider renaming to 'conv_si32_real'" name-notes.txt; then + echo "expected component rename suggestion missing" + exit 1 +fi +if ! grep -q "naming.comp:2: Note: pin name 'value_s32'" name-notes.txt; then + echo "expected pin name note missing (or wrong line number)" + exit 1 +fi +if ! grep -q "naming.comp:3: Note: function name 'conv_s32_s32' mentions legacy HAL type 's32'" name-notes.txt; then + echo "expected function name note missing (or duplicate segments not deduped)" + exit 1 +fi +if grep -q "'s32', 's32'" name-notes.txt; then + echo "duplicate type segments were not deduped in the note" + exit 1 +fi +if ! grep -q "3 name(s) mention a legacy HAL type (rename is optional)" name-notes.txt; then + echo "rename note count missing from the naming.comp summary" + exit 1 +fi +for pat in "component conv_s32_float;" "pin out real value_s32;" "function conv_s32_s32;"; do + if ! grep -q "$pat" naming.comp; then + echo "rename note must not modify the name: $pat" + exit 1 + fi +done + +# a fully converted component with stale names reports the note count +# only: no mechanical-change summary and no diff-review advice +cat > stale.comp <<'EOF' +component stale; +pin out real out_s32; +function _; +license "GPL"; +;; +FUNCTION(_) { + out_s32_set(1.0); +} +EOF +halcompupdate stale.comp >/dev/null 2>stale-notes.txt +if grep -q "mechanical change" stale-notes.txt; then + echo "already-converted component must not report mechanical changes" + exit 1 +fi +if grep -q "review the diff" stale-notes.txt; then + echo "already-converted component must not get diff-review advice" + exit 1 +fi +if ! grep -q "stale.comp: 1 name(s) mention a legacy HAL type (rename is optional)" stale-notes.txt; then + echo "notes-only summary line missing" + exit 1 +fi +if ! grep -q "^stale.comp:2: Note: pin name 'out_s32'" stale-notes.txt; then + echo "expected note with correct line number" + exit 1 +fi + # writes to pins/params in EXTRA_SETUP become setters on references that # halcompile initializes only after extra_setup() runs, so the converted # component would crash at load: the conversion must still happen and a