From be85d8aaf91fcd946f774cb0897ac6742fd64c96 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Fri, 5 Jun 2026 17:35:07 -0400 Subject: [PATCH 1/2] fix(runtime): make user-facing headers compile under gnu++14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User code that includes the strucpp runtime through a generated `c_blocks_code.cpp` (openplc-editor's Arduino flow) compiles under whatever `-std=` the platform's Arduino core picked. Most cores use gnu++17 or later, but **every mbed-based Arduino core** — `mbed_nano` (Nano RP2040 Connect, Nano 33 BLE), `mbed_opta` (Opta), `mbed_giga` (GIGA), `mbed_portenta`, `mbed_edge` — hard-codes `-std=gnu++14` in its `cxxflags.txt`. On those targets every C/C++ POU compile fails with a cascade of `'is_convertible_v' is not a member of 'std'` etc. out of `iec_var.hpp` / `iec_string.hpp` / `iec_traits.hpp`. The strucpp runtime tests + the precompile stage of the openplc build keep using C++17, so this commit doesn't downgrade anything there — it just removes incidental C++17 usage from the four headers reachable from `c_blocks_code.cpp`'s transitive include chain. Substitutions (purely syntactic, no semantic change): iec_var.hpp: - `std::is_X_v` (×8) → `std::is_X::value` iec_string.hpp: - `std::is_X_v` (×3) → `std::is_X::value` - `TO_STRING(integral)` rewritten from `if constexpr` (C++17) into two SFINAE'd overloads (signed vs unsigned). Same behaviour; tag-dispatch is C++11-clean. iec_traits.hpp: - `std::is_X_v` (×3) → `std::is_X::value` - `std::bool_constant` (×2) → `std::integral_constant` - `template` (×4) → typed bounds `template` (subrange specs + forward decls) - 14 `inline constexpr ...` variable templates → `constexpr ...` (inline variables are C++17; variable templates without `inline` are C++14 and work for header-only metaprogramming where the address is never taken) iec_subrange.hpp: - Primary `IEC_SUBRANGE_Value` / `IEC_SUBRANGE_Var` template heads and the matching `IEC_SUBRANGE` alias switched from `auto Lower, auto Upper` to `BaseType Lower, BaseType Upper`. IEC subrange bounds are always the base type by the standard, so this is no capability loss and keeps the declaration consistent with the forward decls in `iec_traits.hpp`. ABI verification: compiled the headers under both `-std=gnu++14` and `-std=gnu++17` against a c_blocks_code.cpp-equivalent translation unit; both produce byte-identical 2552-byte `.o` files. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/runtime/include/iec_string.hpp | 28 +++++++++---- src/runtime/include/iec_subrange.hpp | 14 +++---- src/runtime/include/iec_traits.hpp | 61 +++++++++++++++------------- src/runtime/include/iec_var.hpp | 16 ++++---- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/runtime/include/iec_string.hpp b/src/runtime/include/iec_string.hpp index 060c9993..780af4d1 100644 --- a/src/runtime/include/iec_string.hpp +++ b/src/runtime/include/iec_string.hpp @@ -892,21 +892,33 @@ inline bool NE_STRING(const IECString& s1, const IECString& s2 // ============================================================================= // Numeric → STRING (uses snprintf for real-time safety, no std::to_string) +// +// Split into two SFINAE'd overloads (signed vs unsigned) rather than a single +// function with `if constexpr`, because user C/C++ POU code in +// `c_blocks_code.cpp` compiles under whatever -std= the platform's Arduino +// core picked (gnu++14 on mbed cores). `if constexpr` is C++17-only and +// would make this header unusable from those compilation units. Tag +// dispatch via `enable_if_t` works back to C++11. template()))>, int> = 0> + std::enable_if_t()))>::value + && std::is_unsigned()))>::value, int> = 0> inline IECString<254> TO_STRING(T v) noexcept { char buf[32]; - auto raw = iec_unwrap(v); - if constexpr (std::is_unsigned_v) { - std::snprintf(buf, sizeof(buf), "%llu", static_cast(raw)); - } else { - std::snprintf(buf, sizeof(buf), "%lld", static_cast(raw)); - } + std::snprintf(buf, sizeof(buf), "%llu", static_cast(iec_unwrap(v))); + return IECString<254>(buf); +} + +template()))>::value + && !std::is_unsigned()))>::value, int> = 0> +inline IECString<254> TO_STRING(T v) noexcept { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%lld", static_cast(iec_unwrap(v))); return IECString<254>(buf); } template()))>, int> = 0> + std::enable_if_t()))>::value, int> = 0> inline IECString<254> TO_STRING(T v) noexcept { char buf[64]; std::snprintf(buf, sizeof(buf), "%g", static_cast(iec_unwrap(v))); diff --git a/src/runtime/include/iec_subrange.hpp b/src/runtime/include/iec_subrange.hpp index c784cd05..b7feb2e0 100644 --- a/src/runtime/include/iec_subrange.hpp +++ b/src/runtime/include/iec_subrange.hpp @@ -25,12 +25,12 @@ namespace strucpp { * @tparam Lower The minimum allowed value (inclusive) * @tparam Upper The maximum allowed value (inclusive) */ -template +template class IEC_SUBRANGE_Value { public: using base_type = BaseType; - static constexpr auto lower_bound = Lower; - static constexpr auto upper_bound = Upper; + static constexpr BaseType lower_bound = Lower; + static constexpr BaseType upper_bound = Upper; private: BaseType value_; @@ -208,13 +208,13 @@ class IEC_SUBRANGE_Value { * @tparam Lower The minimum allowed value * @tparam Upper The maximum allowed value */ -template +template class IEC_SUBRANGE_Var { public: using value_type = IEC_SUBRANGE_Value; using base_type = BaseType; - static constexpr auto lower_bound = Lower; - static constexpr auto upper_bound = Upper; + static constexpr BaseType lower_bound = Lower; + static constexpr BaseType upper_bound = Upper; private: value_type value_; @@ -323,7 +323,7 @@ class IEC_SUBRANGE_Var { * Convenience alias for subrange with forcing support. * Usage: IEC_SUBRANGE percentage; */ -template +template using IEC_SUBRANGE = IEC_SUBRANGE_Var; /* diff --git a/src/runtime/include/iec_traits.hpp b/src/runtime/include/iec_traits.hpp index 9eae9a73..4ba83225 100644 --- a/src/runtime/include/iec_traits.hpp +++ b/src/runtime/include/iec_traits.hpp @@ -30,8 +30,13 @@ template class class IEC_STRUCT_Base; template class IEC_ENUM_Value; template class IEC_ENUM_Var; -template class IEC_SUBRANGE_Value; -template class IEC_SUBRANGE_Var; +// Forward declarations. `auto` template parameters are C++17; we declare +// these subrange templates with `BaseType`-typed bounds so they remain +// compilable under the platform's gnu++14 default (mbed Arduino cores). +// Numeric bounds in IEC subrange types are always the same type as the +// base, so requiring `Lower`/`Upper` to be `BaseType` is no semantic loss. +template class IEC_SUBRANGE_Value; +template class IEC_SUBRANGE_Var; template class IEC_REF_TO; // ============================================================================= @@ -232,43 +237,43 @@ struct is_any_elementary> : is_any_elementary {}; // ============================================================================= template -inline constexpr bool is_iec_type_v = is_iec_type::value; +constexpr bool is_iec_type_v = is_iec_type::value; template -inline constexpr bool is_any_bool_v = is_any_bool::value; +constexpr bool is_any_bool_v = is_any_bool::value; template -inline constexpr bool is_any_sint_v = is_any_sint::value; +constexpr bool is_any_sint_v = is_any_sint::value; template -inline constexpr bool is_any_uint_v = is_any_uint::value; +constexpr bool is_any_uint_v = is_any_uint::value; template -inline constexpr bool is_any_int_v = is_any_int::value; +constexpr bool is_any_int_v = is_any_int::value; template -inline constexpr bool is_any_real_v = is_any_real::value; +constexpr bool is_any_real_v = is_any_real::value; template -inline constexpr bool is_any_num_v = is_any_num::value; +constexpr bool is_any_num_v = is_any_num::value; template -inline constexpr bool is_any_bit_v = is_any_bit::value; +constexpr bool is_any_bit_v = is_any_bit::value; template -inline constexpr bool is_any_string_v = is_any_string::value; +constexpr bool is_any_string_v = is_any_string::value; template -inline constexpr bool is_any_date_v = is_any_date::value; +constexpr bool is_any_date_v = is_any_date::value; template -inline constexpr bool is_any_time_v = is_any_time::value; +constexpr bool is_any_time_v = is_any_time::value; template -inline constexpr bool is_any_magnitude_v = is_any_magnitude::value; +constexpr bool is_any_magnitude_v = is_any_magnitude::value; template -inline constexpr bool is_any_elementary_v = is_any_elementary::value; +constexpr bool is_any_elementary_v = is_any_elementary::value; // ============================================================================= // Type Size Traits @@ -297,7 +302,7 @@ template struct iec_bit_size> : iec_bit_size {}; template -inline constexpr size_t iec_bit_size_v = iec_bit_size::value; +constexpr size_t iec_bit_size_v = iec_bit_size::value; // ============================================================================= // Underlying Type Traits @@ -315,10 +320,10 @@ using iec_underlying_type_t = typename iec_underlying_type::type; /** Extract the raw value from an IECVar or pass through a raw value unchanged */ template -inline constexpr T iec_unwrap(T v) noexcept { return v; } +constexpr T iec_unwrap(T v) noexcept { return v; } template -inline constexpr T iec_unwrap(const IECVar& v) noexcept { return v.get(); } +constexpr T iec_unwrap(const IECVar& v) noexcept { return v.get(); } // ============================================================================= // Type Limits @@ -389,16 +394,16 @@ template struct is_iec_array> : std::true_type {}; template -inline constexpr bool is_iec_array_v = is_iec_array::value; +constexpr bool is_iec_array_v = is_iec_array::value; /** Check if T is an IEC struct type */ // Uses std::is_base_of to detect types derived from IEC_STRUCT_Base // Note: Generated structs inherit from IEC_STRUCT_Base template -struct is_iec_struct : std::bool_constant> {}; +struct is_iec_struct : std::integral_constant::value> {}; template -inline constexpr bool is_iec_struct_v = is_iec_struct::value; +constexpr bool is_iec_struct_v = is_iec_struct::value; /** Check if T is an IEC enumeration type */ template struct is_iec_enum : std::false_type {}; @@ -410,19 +415,19 @@ template struct is_iec_enum> : std::true_type {}; template -inline constexpr bool is_iec_enum_v = is_iec_enum::value; +constexpr bool is_iec_enum_v = is_iec_enum::value; /** Check if T is an IEC subrange type */ template struct is_iec_subrange : std::false_type {}; -template +template struct is_iec_subrange> : std::true_type {}; -template +template struct is_iec_subrange> : std::true_type {}; template -inline constexpr bool is_iec_subrange_v = is_iec_subrange::value; +constexpr bool is_iec_subrange_v = is_iec_subrange::value; /** Check if T is an IEC pointer type (REF_TO) */ template struct is_iec_pointer : std::false_type {}; @@ -431,11 +436,11 @@ template struct is_iec_pointer> : std::true_type {}; template -inline constexpr bool is_iec_pointer_v = is_iec_pointer::value; +constexpr bool is_iec_pointer_v = is_iec_pointer::value; /** Check if T is ANY_DERIVED (composite types: arrays, structs, enums, subranges, pointers) */ template -struct is_any_derived : std::bool_constant< +struct is_any_derived : std::integral_constant::value || is_iec_struct::value || is_iec_enum::value || @@ -444,7 +449,7 @@ struct is_any_derived : std::bool_constant< > {}; template -inline constexpr bool is_any_derived_v = is_any_derived::value; +constexpr bool is_any_derived_v = is_any_derived::value; // ============================================================================= // C++17 SFINAE Helpers diff --git a/src/runtime/include/iec_var.hpp b/src/runtime/include/iec_var.hpp index 8dbd519a..b7cbef12 100644 --- a/src/runtime/include/iec_var.hpp +++ b/src/runtime/include/iec_var.hpp @@ -59,7 +59,7 @@ class IECVar { * to functions expecting a wider IECVar type. Without this, C++ would need * two user-defined conversions (IECVar→U→T→IECVar) which is disallowed. */ template && !std::is_same_v, int> = 0> + std::is_convertible::value && !std::is_same::value, int> = 0> IECVar(const IECVar& other) noexcept : value_{static_cast(other.get())}, forced_{false}, forced_value_{} {} @@ -197,7 +197,7 @@ class IECVar { * by providing a direct match (template is preferred over two indirect paths * that each require one user-defined conversion). */ template && !std::is_same_v, int> = 0> + std::is_convertible::value && !std::is_same::value, int> = 0> IECVar& operator=(const IECVar& other) noexcept { set(static_cast(other.get())); return *this; @@ -207,7 +207,7 @@ class IECVar { * WARNING: On 64-bit platforms, assigning to types narrower than pointer width * (e.g., DWORD) truncates the address. Use ULINT, LWORD, or PTR_INT_t for * portable pointer-to-integer storage. */ - template, int> = 0> + template::value, int> = 0> IECVar& operator=(const IEC_Ptr& ptr) noexcept { set(static_cast(ptr.to_addr())); return *this; @@ -218,10 +218,10 @@ class IECVar { // ========================================================================= /** Forward operator-> to underlying type (struct/FB member access) */ - template, int> = 0> + template::value, int> = 0> T* operator->() noexcept { return &value_; } - template, int> = 0> + template::value, int> = 0> const T* operator->() const noexcept { return &value_; } /** Forward operator[] to underlying type (1D array access) */ @@ -350,7 +350,7 @@ inline IECVar operator/(const IECVar& a, const IECVar& b) noexcept { return IECVar(a.get() / b.get()); } -template>> +template::value>> inline IECVar operator%(const IECVar& a, const IECVar& b) noexcept { return IECVar(a.get() % b.get()); } @@ -364,8 +364,8 @@ template inline IECVar operator*(const IECVar& a, T b) noexcep template inline IECVar operator*(T a, const IECVar& b) noexcept { return IECVar(a * b.get()); } template inline IECVar operator/(const IECVar& a, T b) noexcept { return IECVar(a.get() / b); } template inline IECVar operator/(T a, const IECVar& b) noexcept { return IECVar(a / b.get()); } -template>> inline IECVar operator%(const IECVar& a, T b) noexcept { return IECVar(a.get() % b); } -template>> inline IECVar operator%(T a, const IECVar& b) noexcept { return IECVar(a % b.get()); } +template::value>> inline IECVar operator%(const IECVar& a, T b) noexcept { return IECVar(a.get() % b); } +template::value>> inline IECVar operator%(T a, const IECVar& b) noexcept { return IECVar(a % b.get()); } // ============================================================================= // Comparison Operators From de7475aa176776940b4b1c1f72095488f9247776 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Fri, 5 Jun 2026 17:44:38 -0400 Subject: [PATCH 2/2] chore(release): v0.5.5 Patch release: fix(runtime): make user-facing headers compile under gnu++14 so c_blocks_code.cpp builds under mbed-based Arduino cores (Nano RP2040 Connect, Opta, Nano 33 BLE, GIGA, Portenta, Edge Control). See #168. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a117be7d..47fb48b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "strucpp", - "version": "0.5.4", + "version": "0.5.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "strucpp", - "version": "0.5.4", + "version": "0.5.5", "license": "GPL-3.0-or-later", "dependencies": { "chevrotain": "^11.0.0" diff --git a/package.json b/package.json index a46f8658..b1b83db8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strucpp", - "version": "0.5.4", + "version": "0.5.5", "description": "IEC 61131-3 Structured Text to C++ Compiler", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -99,4 +99,4 @@ "typescript": "^5.0.0", "vitest": "^1.0.0" } -} \ No newline at end of file +}