wrps-demo-kit/03-plc/as-built/strucpp_runtime/include/iec_std_lib.hpp
Clio Liu 55279ca78f docs(plc): as-built copy, versions, and a getting-started guide
Adds what the folder was missing for someone picking it up cold.

as-built/  the STruC++ output copied out of the live container - the C++
           the PLC is actually executing, plus program.st and the Modbus
           buffer config. The running image was made with docker commit
           and exists in no registry, so this is the only other copy of
           the compiled form.

           Verified while copying: the deployed program.st matches
           build/wrps.st generated from src/. Identical POU structure,
           ZERO differences in non-declaration lines. src/ is genuinely
           canonical and the running PLC agrees with it.

VERSIONS.md         every version read from the running system, not from
                    documentation: runtime v4.1.10, STruC++ 0.6.2, Editor
                    4.2.11, Debian 12, g++ 12.2, pymodbus 3.11.2, CI
                    Server R1.03. Plus which plugins are enabled -
                    ethercat is on for no reason - and a v3-vs-v4 table,
                    since most OpenPLC guidance online is for v3 and the
                    %MW HR1024 change silently produces wrong data.

GETTING-STARTED.md  three questions answered in a page: how to modify the
                    program, how to stand up a new PLC container from
                    scratch, how to move this one. Includes the compose
                    file, the Editor steps that are not generated and are
                    always missed, and a symptom-to-document table.
2026-09-02 16:48:30 +10:00

1501 lines
54 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later WITH STruCpp-runtime-exception
// Copyright (C) 2025 Autonomy / OpenPLC Project
// This file is part of the STruC++ Runtime Library and is covered by the
// STruC++ Runtime Library Exception. See COPYING.RUNTIME for details.
/**
* STruC++ Runtime - IEC Standard Library
*
* This header provides the standard IEC 61131-3 functions and utilities.
* Functions are implemented as C++ templates with IEC type constraints
* for type safety and compliance with IEC 61131-3 type system.
*
* Type constraints follow IEC 61131-3 ANY type hierarchy:
* - ANY_NUM: Numeric types (integers + reals)
* - ANY_INT: Integer types (signed + unsigned)
* - ANY_REAL: Floating point types (REAL, LREAL)
* - ANY_BIT: Bit string types (BOOL, BYTE, WORD, DWORD, LWORD)
* - ANY_ELEMENTARY: All elementary types
* - ANY_MAGNITUDE: Numeric + time types
*/
#pragma once
#include "iec_var.hpp"
#include "iec_traits.hpp"
#include "iec_retain.hpp"
#include "iec_ptr.hpp"
#include "iec_string.hpp"
#include "iec_wstring.hpp"
// IEC 61131-3 temporal types — pulled in here so the standard
// library entry point exposes every standard function (`ADD_TIME`,
// `ADD_DATE`, `ADD_DT`, `ADD_TOD`, `CONCAT_DATE_TOD`, etc.) without
// the caller having to chase the right per-type header. `codegen.ts`
// emits a single `#include "iec_std_lib.hpp"` into every
// generated.hpp, so the only way a generated POU using TIME or
// calendar arithmetic can resolve those symbols is through this
// transitive chain. Header guards make each include idempotent.
#include "iec_time.hpp"
#include "iec_date.hpp"
#include "iec_dt.hpp"
#include "iec_tod.hpp"
#include <cmath>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <type_traits>
// Undefine AVR `<time.h>` macros that collide with common IEC
// identifiers. `<chrono>` above pulls in `<ctime>` → `<time.h>`,
// and AVR-libc's `time.h` defines a handful of all-caps duration /
// epoch constants that would silently rewrite a user's similarly-
// named variable into a numeric literal before the C++ parser sees
// it. The collisions surface as cryptic `expected unqualified-id
// before numeric constant` errors on lines like
// `IEC_TIME ONE_HOUR;`.
//
// Same pattern as the `#undef OVERFLOW` codegen emits into every
// `generated.hpp` to neutralise `<math.h>`'s SVID error code.
// Header guards make each include idempotent, so doing the undef
// right after the time-family includes is fine: any later include
// of `<time.h>` directly would re-define the macros, but no
// strucpp-side header does that.
#undef ONE_HOUR
#undef ONE_DEGREE
#undef ONE_DAY
#undef UNIX_OFFSET
#undef NTP_OFFSET
namespace strucpp {
// =============================================================================
// Base Classes for Runtime
// =============================================================================
// Forward declaration for retain support
struct RetainVarInfo;
/**
* Base class for all program instances.
* Provides the interface for the runtime scheduler.
*/
struct ProgramBase {
virtual ~ProgramBase() = default;
/** Execute one cycle of the program */
virtual void run() = 0;
/**
* Get the array of retain variable descriptors.
* Override in generated code if the program has RETAIN variables.
* @return Pointer to static array, or nullptr if no retain variables
*/
virtual const RetainVarInfo* getRetainVars() const { return nullptr; }
/**
* Get the number of retain variables.
* Override in generated code if the program has RETAIN variables.
* @return Count of retain variables
*/
virtual size_t getRetainCount() const { return 0; }
// -------------------------------------------------------------------------
// Threaded-runtime hooks (appended at the end of the vtable so run() stays
// at slot 1 -- a runtime that predates these still works, it just never
// calls them). No-ops by default; generated code overrides them ONLY when
// compiled with STRUCPP_THREADED. The OpenPLC threaded runtime calls
// sync_in() before run() and sync_out() after, around a per-task private
// working copy of this program's VAR_EXTERNAL globals (so task bodies run
// without holding a global lock). located_range() reports this program's
// contiguous slice of the global locatedVars[] table so the runtime can
// copy its located I/O in/out scoped to the owning task.
// -------------------------------------------------------------------------
/** Copy this program's VAR_EXTERNAL globals from canonical storage into
* its private working copies. Called by the runtime before run(). */
virtual void sync_in() {}
/** Commit this program's changed VAR_EXTERNAL globals from its working
* copies back to canonical storage. Called by the runtime after run(). */
virtual void sync_out() {}
/** Report this program's slice [offset, offset+count) of the project-wide
* locatedVars[] table. count == 0 means the program has no located I/O. */
virtual void located_range(uint32_t* offset, uint32_t* count) const {
*offset = 0;
*count = 0;
}
};
/**
* Task instance descriptor.
* Describes a task's scheduling properties and associated program instances.
*/
struct TaskInstance {
const char* name; ///< Task name
int64_t interval_ns; ///< Execution interval in nanoseconds (0 = event-driven)
int32_t priority; ///< Task priority (higher = more important)
ProgramBase** programs; ///< Array of program instances for this task
size_t program_count; ///< Number of programs in this task
TaskInstance() noexcept
: name(nullptr), interval_ns(0), priority(0), programs(nullptr), program_count(0) {}
TaskInstance(const char* n, int64_t interval, int32_t prio,
ProgramBase** progs, size_t count) noexcept
: name(n), interval_ns(interval), priority(prio), programs(progs), program_count(count) {}
};
/**
* Resource instance descriptor.
* Describes a resource (processor) and its associated tasks.
*/
struct ResourceInstance {
const char* name; ///< Resource name
const char* processor; ///< Processor type (from ON clause)
TaskInstance* tasks; ///< Array of tasks in this resource
size_t task_count; ///< Number of tasks
ResourceInstance() noexcept
: name(nullptr), processor(nullptr), tasks(nullptr), task_count(0) {}
ResourceInstance(const char* n, const char* proc,
TaskInstance* t, size_t count) noexcept
: name(n), processor(proc), tasks(t), task_count(count) {}
};
/**
* Base class for configuration instances.
* Provides the interface for the runtime to access project structure.
*/
struct ConfigurationInstance {
virtual ~ConfigurationInstance() = default;
/** Get configuration name */
virtual const char* get_name() const = 0;
/** Get array of resources */
virtual ResourceInstance* get_resources() = 0;
/** Get number of resources */
virtual size_t get_resource_count() const = 0;
};
// =============================================================================
// Numeric Functions (ANY_NUM -> ANY_NUM, or ANY_REAL -> ANY_REAL)
// =============================================================================
/**
* ABS - Absolute value
* Input: ANY_NUM, Output: ANY_NUM (same type)
*/
template<typename T, enable_if_any_num<T> = 0>
inline T ABS(T value) noexcept {
auto v = iec_unwrap(value);
if constexpr (std::is_floating_point_v<decltype(v)>) {
return T(std::abs(v));
} else if constexpr (std::is_signed_v<decltype(v)>) {
return T(v < 0 ? -v : v);
} else {
return value;
}
}
/**
* SQRT - Square root
* Input: ANY_REAL, Output: ANY_REAL (same type)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T SQRT(T value) noexcept {
return T(std::sqrt(static_cast<double>(iec_unwrap(value))));
}
/**
* LN - Natural logarithm
* Input: ANY_REAL, Output: ANY_REAL (same type)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T LN(T value) noexcept {
return T(std::log(static_cast<double>(iec_unwrap(value))));
}
/**
* LOG - Base-10 logarithm
* Input: ANY_REAL, Output: ANY_REAL (same type)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T LOG(T value) noexcept {
return T(std::log10(static_cast<double>(iec_unwrap(value))));
}
/**
* EXP - Exponential (e^x)
* Input: ANY_REAL, Output: ANY_REAL (same type)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T EXP(T value) noexcept {
return T(std::exp(static_cast<double>(iec_unwrap(value))));
}
/**
* EXPT - Exponentiation (base^exponent)
* Input: ANY_REAL, Output: ANY_REAL (same type)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T EXPT(T base, T exponent) noexcept {
return T(std::pow(static_cast<double>(iec_unwrap(base)), static_cast<double>(iec_unwrap(exponent))));
}
// Mixed-type EXPT: allows e.g. EXPT(INT, REAL) → returns LREAL
template<typename T1, typename T2,
typename = std::enable_if_t<!std::is_same_v<std::decay_t<T1>, std::decay_t<T2>>>>
inline IEC_LREAL EXPT(T1 base, T2 exponent) noexcept {
return IEC_LREAL(std::pow(static_cast<double>(iec_unwrap(base)), static_cast<double>(iec_unwrap(exponent))));
}
// Same-type EXPT for non-REAL types (CODESYS extension: EXPT(INT, INT))
template<typename T,
std::enable_if_t<(is_any_int_v<T> || is_any_bit_v<T>) && !is_any_real_v<T>, int> = 0>
inline IEC_LREAL EXPT(T base, T exponent) noexcept {
return IEC_LREAL(std::pow(static_cast<double>(iec_unwrap(base)), static_cast<double>(iec_unwrap(exponent))));
}
// =============================================================================
// Trigonometric Functions (ANY_REAL -> ANY_REAL)
// =============================================================================
/**
* SIN - Sine
* Input: ANY_REAL (radians), Output: ANY_REAL
*/
template<typename T, enable_if_any_real<T> = 0>
inline T SIN(T value) noexcept {
return T(std::sin(static_cast<double>(iec_unwrap(value))));
}
/**
* COS - Cosine
* Input: ANY_REAL (radians), Output: ANY_REAL
*/
template<typename T, enable_if_any_real<T> = 0>
inline T COS(T value) noexcept {
return T(std::cos(static_cast<double>(iec_unwrap(value))));
}
/**
* TAN - Tangent
* Input: ANY_REAL (radians), Output: ANY_REAL
*/
template<typename T, enable_if_any_real<T> = 0>
inline T TAN(T value) noexcept {
return T(std::tan(static_cast<double>(iec_unwrap(value))));
}
/**
* ASIN - Arc sine
* Input: ANY_REAL, Output: ANY_REAL (radians)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T ASIN(T value) noexcept {
return T(std::asin(static_cast<double>(iec_unwrap(value))));
}
/**
* ACOS - Arc cosine
* Input: ANY_REAL, Output: ANY_REAL (radians)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T ACOS(T value) noexcept {
return T(std::acos(static_cast<double>(iec_unwrap(value))));
}
/**
* ATAN - Arc tangent
* Input: ANY_REAL, Output: ANY_REAL (radians)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T ATAN(T value) noexcept {
return T(std::atan(static_cast<double>(iec_unwrap(value))));
}
/**
* ATAN2 - Arc tangent of y/x (two-argument form)
* Input: ANY_REAL, Output: ANY_REAL (radians between -PI and PI)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T ATAN2(T y, T x) noexcept {
return T(std::atan2(static_cast<double>(iec_unwrap(y)), static_cast<double>(iec_unwrap(x))));
}
/**
* TRUNC - Truncate toward zero
* Input: ANY_REAL, Output: ANY_REAL (integer part)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T TRUNC(T value) noexcept {
return T(std::trunc(static_cast<double>(iec_unwrap(value))));
}
/**
* ROUND - Round to nearest integer
* Input: ANY_REAL, Output: ANY_REAL
* Rounds half away from zero (banker's rounding not used)
*/
template<typename T, enable_if_any_real<T> = 0>
inline T ROUND(T value) noexcept {
return T(std::round(static_cast<double>(iec_unwrap(value))));
}
// =============================================================================
// Selection Functions (ANY_ELEMENTARY for comparisons)
// =============================================================================
/**
* SEL - Binary selection
* Input: BOOL selector, ANY values, Output: ANY (same type as inputs)
* Returns in1 if g is FALSE, in0 if g is TRUE
*/
template<typename T>
inline T SEL(IEC_BOOL g, T in0, T in1) noexcept {
return iec_unwrap(g) ? in1 : in0;
}
/**
* MAX - Maximum of two values
* Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type)
*/
template<typename T, enable_if_any_elementary<T> = 0>
inline T MAX(T a, T b) noexcept {
return iec_unwrap(a) > iec_unwrap(b) ? a : b;
}
/**
* MIN - Minimum of two values
* Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type)
*/
template<typename T, enable_if_any_elementary<T> = 0>
inline T MIN(T a, T b) noexcept {
return iec_unwrap(a) < iec_unwrap(b) ? a : b;
}
/**
* LIMIT - Limit value to range [mn, mx]
* Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type)
*/
template<typename T, enable_if_any_elementary<T> = 0>
inline T LIMIT(T mn, T in, T mx) noexcept {
if (iec_unwrap(in) < iec_unwrap(mn)) return mn;
if (iec_unwrap(in) > iec_unwrap(mx)) return mx;
return in;
}
// Mixed-type MIN/MAX/LIMIT/SEL overloads (OSCAT mixes e.g. INT with DINT)
template<typename T, typename U,
std::enable_if_t<!std::is_same_v<std::decay_t<T>, std::decay_t<U>>, int> = 0>
inline auto MAX(T a, U b) noexcept {
using CT = std::common_type_t<decltype(iec_unwrap(a)), decltype(iec_unwrap(b))>;
auto va = static_cast<CT>(iec_unwrap(a));
auto vb = static_cast<CT>(iec_unwrap(b));
return va > vb ? va : vb;
}
template<typename T, typename U,
std::enable_if_t<!std::is_same_v<std::decay_t<T>, std::decay_t<U>>, int> = 0>
inline auto MIN(T a, U b) noexcept {
using CT = std::common_type_t<decltype(iec_unwrap(a)), decltype(iec_unwrap(b))>;
auto va = static_cast<CT>(iec_unwrap(a));
auto vb = static_cast<CT>(iec_unwrap(b));
return va < vb ? va : vb;
}
template<typename T1, typename T2, typename T3>
inline auto LIMIT(T1 mn, T2 in, T3 mx) noexcept
-> std::enable_if_t<
!(std::is_same_v<std::decay_t<T1>, std::decay_t<T2>> &&
std::is_same_v<std::decay_t<T2>, std::decay_t<T3>>),
std::common_type_t<decltype(iec_unwrap(mn)), decltype(iec_unwrap(in)), decltype(iec_unwrap(mx))>> {
using CT = std::common_type_t<decltype(iec_unwrap(mn)), decltype(iec_unwrap(in)), decltype(iec_unwrap(mx))>;
auto vmn = static_cast<CT>(iec_unwrap(mn));
auto vin = static_cast<CT>(iec_unwrap(in));
auto vmx = static_cast<CT>(iec_unwrap(mx));
if (vin < vmn) return vmn;
if (vin > vmx) return vmx;
return vin;
}
template<typename T, typename U,
std::enable_if_t<!std::is_same_v<std::decay_t<T>, std::decay_t<U>>, int> = 0>
inline auto SEL(IEC_BOOL g, T in0, U in1) noexcept {
using CT = std::common_type_t<decltype(iec_unwrap(in0)), decltype(iec_unwrap(in1))>;
return iec_unwrap(g) ? static_cast<CT>(iec_unwrap(in1)) : static_cast<CT>(iec_unwrap(in0));
}
/**
* MUX - Multiplexer (extensible — IEC 61131-3 minArgs=3, K + at least
* two inputs). Returns the input selected by the zero-based `k`:
*
* MUX(0, A, B, C, D) == A
* MUX(2, A, B, C, D) == C
*
* The single-input terminator `MUX(k, in0)` exists only to anchor
* the variadic recursion; callers should not invoke it directly
* (the IEC contract requires K + ≥2 inputs). When `k` is out of
* range we fall through to the last input, matching the editor's
* legacy 2-input behaviour and consistent with how CODESYS clamps
* over-range selectors.
*/
template<typename T>
inline T MUX([[maybe_unused]] IEC_INT k, T in0) noexcept {
return in0;
}
template<typename T, typename... Args>
inline T MUX(IEC_INT k, T in0, T in1, Args... rest) noexcept {
if (iec_unwrap(k) == 0) return in0;
return MUX(IEC_INT(iec_unwrap(k) - 1), in1, rest...);
}
// =============================================================================
// Comparison Functions (ANY_ELEMENTARY -> BOOL)
// =============================================================================
// Comparison operators take two arguments deduced independently — that way
// `LE(real_var, 0.0)` (where the literal is double / IEC_LREAL) and
// `EQ(my_int, 0)` (where the literal is int / IEC_INT) both type-check
// without forcing the caller to wrap every literal in a cast. Each side
// only has to land on an IEC elementary type after `iec_unwrap`; the
// comparison itself uses C++'s usual arithmetic conversions to find a
// common type.
//
// CONVERSION SEMANTICS — read this before writing cross-sign tests:
// Mixing signed and unsigned operands follows C++'s usual arithmetic
// conversions, not an IEC rule.
//
// - When the unsigned operand has *lower* integer rank than `int`
// (IEC_USINT, IEC_UINT — uint8/uint16), both sides are promoted to
// `int` and the compare happens in signed land. No wrap.
//
// - When the unsigned operand has rank >= `int` (IEC_UDINT, IEC_ULINT —
// uint32/uint64) the signed operand converts to the unsigned type
// and a negative value wraps to a large unsigned. So
// `EQ(IEC_UDINT(0xFFFFFFFFu), -1)` is TRUE because -1 becomes
// 0xFFFFFFFF before the compare.
//
// STruC++ does not insert extra guards: IEC 61131-3 doesn't define
// cross-sign-class comparison, and we want the generated C++ to behave
// predictably under standard rules. If a project needs sign-strict
// comparisons, cast both sides to the same type before calling
// EQ/NE/LT/LE/GT/GE.
template<typename A, typename B>
using enable_if_two_elementary = std::enable_if_t<
is_any_elementary_v<iec_underlying_type_t<std::decay_t<A>>> &&
is_any_elementary_v<iec_underlying_type_t<std::decay_t<B>>>,
int>;
/**
* GT - Greater than
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL GT(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) > iec_unwrap(b));
}
/**
* GE - Greater than or equal
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL GE(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) >= iec_unwrap(b));
}
/**
* EQ - Equal
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL EQ(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) == iec_unwrap(b));
}
/**
* LE - Less than or equal
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL LE(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) <= iec_unwrap(b));
}
/**
* LT - Less than
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL LT(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) < iec_unwrap(b));
}
/**
* NE - Not equal
* Input: ANY_ELEMENTARY, Output: BOOL
*/
template<typename A, typename B, enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL NE(A a, B b) noexcept {
return IEC_BOOL(iec_unwrap(a) != iec_unwrap(b));
}
// ---------------------------------------------------------------------------
// Variadic chain forms for GT / GE / EQ / LE / LT / NE
// ---------------------------------------------------------------------------
//
// IEC 61131-3 defines the comparison functions as extensible:
// `GT(a, b, c)` means `(a > b) AND (b > c)`. These overloads
// implement the chain semantic and live alongside the binary forms
// above so the codegen can emit the same C++ name for any arity.
//
// Same heterogeneous-type signature as the binary forms — every
// adjacent pair goes through `iec_unwrap` independently so mixed
// IECVar / underlying types compare cleanly.
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL GT(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) > iec_unwrap(b))) return IEC_BOOL(false);
return GT(b, c, rest...);
}
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL GE(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) >= iec_unwrap(b))) return IEC_BOOL(false);
return GE(b, c, rest...);
}
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL EQ(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) == iec_unwrap(b))) return IEC_BOOL(false);
return EQ(b, c, rest...);
}
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL LE(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) <= iec_unwrap(b))) return IEC_BOOL(false);
return LE(b, c, rest...);
}
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL LT(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) < iec_unwrap(b))) return IEC_BOOL(false);
return LT(b, c, rest...);
}
template<typename A, typename B, typename C, typename... Rest,
enable_if_two_elementary<A, B> = 0>
inline IEC_BOOL NE(A a, B b, C c, Rest... rest) noexcept {
if (!(iec_unwrap(a) != iec_unwrap(b))) return IEC_BOOL(false);
return NE(b, c, rest...);
}
// =============================================================================
// Bit Shift Functions (ANY_BIT -> ANY_BIT)
// =============================================================================
/**
* SHL - Shift left
* Input: ANY_BIT, ANY_INT (shift count), Output: ANY_BIT
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T SHL(T in, IEC_INT n) noexcept {
auto shift = iec_unwrap(n);
if (shift <= 0) return shift == 0 ? in : T(0);
return T(iec_unwrap(in) << shift);
}
// Mixed-type shift count overloads (OSCAT uses various integer types for shift amount)
template<typename T, typename N,
enable_if_any_bit<T> = 0,
std::enable_if_t<!std::is_same_v<std::decay_t<N>, IEC_INT>, int> = 0>
inline T SHL(T in, N n) noexcept {
auto shift = static_cast<int>(iec_unwrap(n));
if (shift <= 0) return shift == 0 ? in : T(0);
return T(iec_unwrap(in) << shift);
}
/**
* SHR - Shift right
* Input: ANY_BIT, ANY_INT (shift count), Output: ANY_BIT
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T SHR(T in, IEC_INT n) noexcept {
auto shift = iec_unwrap(n);
if (shift <= 0) return shift == 0 ? in : T(0);
return T(iec_unwrap(in) >> shift);
}
// Mixed-type shift count overloads
template<typename T, typename N,
enable_if_any_bit<T> = 0,
std::enable_if_t<!std::is_same_v<std::decay_t<N>, IEC_INT>, int> = 0>
inline T SHR(T in, N n) noexcept {
auto shift = static_cast<int>(iec_unwrap(n));
if (shift <= 0) return shift == 0 ? in : T(0);
return T(iec_unwrap(in) >> shift);
}
// SHL/SHR for signed integer types (CODESYS extension, used by OSCAT)
// IEC standard restricts to ANY_BIT, but CODESYS allows ANY_INT
template<typename T, typename N,
std::enable_if_t<is_any_int_v<T> && !is_any_bit_v<T>, int> = 0>
inline T SHL(T in, N n) noexcept {
auto shift = static_cast<int>(iec_unwrap(n));
if (shift <= 0) return shift == 0 ? in : T(0);
using UT = std::make_unsigned_t<iec_underlying_type_t<T>>;
return T(static_cast<iec_underlying_type_t<T>>(
static_cast<UT>(iec_unwrap(in)) << shift));
}
template<typename T, typename N,
std::enable_if_t<is_any_int_v<T> && !is_any_bit_v<T>, int> = 0>
inline T SHR(T in, N n) noexcept {
auto shift = static_cast<int>(iec_unwrap(n));
if (shift <= 0) return shift == 0 ? in : T(0);
return T(iec_unwrap(in) >> shift);
}
/**
* ROL - Rotate left
* Input: ANY_BIT, ANY_INT (shift count), Output: ANY_BIT
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T ROL(T in, IEC_INT n) noexcept {
constexpr int bits = sizeof(iec_underlying_type_t<T>) * 8;
auto v = iec_unwrap(in);
auto shift = iec_unwrap(n) % bits;
if (shift < 0) shift += bits; // IEC 61131-3: negative N reverses direction
if (shift == 0) return in;
return T((v << shift) | (v >> (bits - shift)));
}
// Mixed-type rotate overloads
template<typename T, typename N,
enable_if_any_bit<T> = 0,
std::enable_if_t<!std::is_same_v<std::decay_t<N>, IEC_INT>, int> = 0>
inline T ROL(T in, N n) noexcept {
constexpr int bits = sizeof(iec_underlying_type_t<T>) * 8;
auto v = iec_unwrap(in);
auto shift = static_cast<int>(iec_unwrap(n)) % bits;
if (shift < 0) shift += bits; // IEC 61131-3: negative N reverses direction
if (shift == 0) return in;
return T((v << shift) | (v >> (bits - shift)));
}
/**
* ROR - Rotate right
* Input: ANY_BIT, ANY_INT (shift count), Output: ANY_BIT
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T ROR(T in, IEC_INT n) noexcept {
constexpr int bits = sizeof(iec_underlying_type_t<T>) * 8;
auto v = iec_unwrap(in);
auto shift = iec_unwrap(n) % bits;
if (shift < 0) shift += bits; // IEC 61131-3: negative N reverses direction
if (shift == 0) return in;
return T((v >> shift) | (v << (bits - shift)));
}
// Mixed-type rotate overloads
template<typename T, typename N,
enable_if_any_bit<T> = 0,
std::enable_if_t<!std::is_same_v<std::decay_t<N>, IEC_INT>, int> = 0>
inline T ROR(T in, N n) noexcept {
constexpr int bits = sizeof(iec_underlying_type_t<T>) * 8;
auto v = iec_unwrap(in);
auto shift = static_cast<int>(iec_unwrap(n)) % bits;
if (shift < 0) shift += bits; // IEC 61131-3: negative N reverses direction
if (shift == 0) return in;
return T((v >> shift) | (v << (bits - shift)));
}
// =============================================================================
// Type Conversion Functions
// =============================================================================
/**
* Helper: round-then-cast for REAL→integer conversions per IEC 61131-3
*/
template<typename ToVal, typename FromVal>
inline ToVal iec_convert_value(FromVal value) noexcept {
// IEC 61131-3: REAL/LREAL to integer types use rounding (nearest)
if constexpr (std::is_floating_point_v<FromVal> && std::is_integral_v<ToVal>) {
return static_cast<ToVal>(std::round(static_cast<double>(value)));
} else {
return static_cast<ToVal>(value);
}
}
/**
* Generic type conversion (IECVar → IECVar)
*/
template<typename To, typename From>
inline auto CONVERT(From value) noexcept
-> std::enable_if_t<!std::is_arithmetic_v<From>, To> {
return To(iec_convert_value<typename To::value_type>(iec_unwrap(value)));
}
/**
* Generic type conversion (arithmetic → IECVar)
*/
template<typename To, typename From>
inline auto CONVERT(From value) noexcept
-> std::enable_if_t<std::is_arithmetic_v<From>, To> {
return To(iec_convert_value<typename To::value_type>(value));
}
// Specific conversion functions (aliases for clarity)
template<typename T> inline IEC_BOOL TO_BOOL(T v) noexcept { return CONVERT<IEC_BOOL>(v); }
template<typename T> inline IEC_SINT TO_SINT(T v) noexcept { return CONVERT<IEC_SINT>(v); }
template<typename T> inline IEC_INT TO_INT(T v) noexcept { return CONVERT<IEC_INT>(v); }
template<typename T> inline IEC_DINT TO_DINT(T v) noexcept { return CONVERT<IEC_DINT>(v); }
template<typename T> inline IEC_LINT TO_LINT(T v) noexcept { return CONVERT<IEC_LINT>(v); }
template<typename T> inline IEC_USINT TO_USINT(T v) noexcept { return CONVERT<IEC_USINT>(v); }
template<typename T> inline IEC_UINT TO_UINT(T v) noexcept { return CONVERT<IEC_UINT>(v); }
template<typename T> inline IEC_UDINT TO_UDINT(T v) noexcept { return CONVERT<IEC_UDINT>(v); }
template<typename T> inline IEC_ULINT TO_ULINT(T v) noexcept { return CONVERT<IEC_ULINT>(v); }
template<typename T> inline IEC_REAL TO_REAL(T v) noexcept { return CONVERT<IEC_REAL>(v); }
template<typename T> inline IEC_LREAL TO_LREAL(T v) noexcept { return CONVERT<IEC_LREAL>(v); }
template<typename T> inline IEC_BYTE TO_BYTE(T v) noexcept { return CONVERT<IEC_BYTE>(v); }
template<typename T> inline IEC_WORD TO_WORD(T v) noexcept { return CONVERT<IEC_WORD>(v); }
template<typename T> inline IEC_DWORD TO_DWORD(T v) noexcept { return CONVERT<IEC_DWORD>(v); }
template<typename T> inline IEC_LWORD TO_LWORD(T v) noexcept { return CONVERT<IEC_LWORD>(v); }
// Time/Date conversion functions
// All time types are int64_t aliases, so IEC_TIME/IEC_DATE/IEC_TOD/IEC_DT
// are all IECVar<int64_t>. We use a single template for each target type.
// OSCAT calls TO_TIME with integer values (ms) — we convert ms → ns.
// For TIME→TIME (same underlying type), the static_cast is identity and
// the multiply still applies, but this matches CODESYS behavior where
// integer values passed to TO_TIME are treated as milliseconds.
template<typename T> inline IEC_TIME TO_TIME(T v) noexcept {
// If the input is already an IECVar<int64_t> (TIME/DATE/DT/TOD), this
// treats the raw nanosecond value as milliseconds — but in practice
// OSCAT only calls TO_TIME on integer types, not on TIME values.
return IEC_TIME(static_cast<TIME_t>(iec_unwrap(v)) * 1000000);
}
template<typename T> inline IEC_DATE TO_DATE(T v) noexcept {
return IEC_DATE(static_cast<DATE_t>(iec_unwrap(v)));
}
template<typename T> inline IEC_DT TO_DT(T v) noexcept {
return IEC_DT(static_cast<DT_t>(iec_unwrap(v)));
}
template<typename T> inline IEC_TOD TO_TOD(T v) noexcept {
return IEC_TOD(static_cast<TOD_t>(iec_unwrap(v)));
}
// ---------------------------------------------------------------------------
// STRING -> TIME / TOD / DATE / DT parsing
//
// The frontend lowers STRING_TO_TIME / STRING_TO_TOD / STRING_TO_DATE /
// STRING_TO_DT to TO_TIME / TO_TOD / TO_DATE / TO_DT. The numeric overloads
// above treat their argument as a raw count; the string overloads below PARSE
// the textual IEC literal (used by e.g. OSCAT's TIMER_EVENT_DECODE). Formats,
// each with an optional `PREFIX#`:
// TIME : [T#] (<num>(d|h|m|s|ms|us|ns))+ -> nanoseconds
// TOD : [TOD#] HH:MM[:SS[.fff]] -> ns since midnight
// DATE : [D#] YYYY-MM-DD -> days since 1970-01-01
// DT : [DT#] YYYY-MM-DD-HH:MM[:SS[.fff]] -> ns since the Unix epoch
// Lenient and exception-free (AVR-safe); unparseable input yields 0.
namespace iec_strparse {
// Skip a leading `IDENT#` literal prefix (e.g. "T#", "TOD#") if present.
inline const char* skip_literal_prefix(const char* s) noexcept {
for (const char* p = s; *p; ++p) {
if (*p == '#') return p + 1;
const char c = *p;
const bool idish = c == '_' || (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
if (!idish) break;
}
return s;
}
inline int64_t parse_time_ns(const char* s) noexcept {
s = skip_literal_prefix(s);
int64_t total = 0;
while (*s) {
char* end = nullptr;
const double val = std::strtod(s, &end);
if (end == s) { ++s; continue; }
s = end;
const char u0 = (*s >= 'A' && *s <= 'Z') ? static_cast<char>(*s + 32) : *s;
const char u1 =
(s[0] && s[1] >= 'A' && s[1] <= 'Z') ? static_cast<char>(s[1] + 32) : s[1];
int64_t mult = 1000000LL; // default unit: milliseconds
if (u0 == 'm' && u1 == 's') { mult = 1000000LL; s += 2; }
else if (u0 == 'u' && u1 == 's') { mult = 1000LL; s += 2; }
else if (u0 == 'n' && u1 == 's') { mult = 1LL; s += 2; }
else if (u0 == 'd') { mult = 86400000000000LL; s += 1; }
else if (u0 == 'h') { mult = 3600000000000LL; s += 1; }
else if (u0 == 'm') { mult = 60000000000LL; s += 1; }
else if (u0 == 's') { mult = 1000000000LL; s += 1; }
total += static_cast<int64_t>(val * static_cast<double>(mult));
}
return total;
}
inline void read_int(const char*& s, long long& out) noexcept {
char* end = nullptr;
out = std::strtoll(s, &end, 10);
if (end != s) s = end;
}
inline int64_t parse_tod_ns(const char* s) noexcept {
s = skip_literal_prefix(s);
long long hh = 0, mm = 0, ss = 0;
double frac = 0;
read_int(s, hh);
if (*s == ':') { ++s; read_int(s, mm); }
if (*s == ':') { ++s; read_int(s, ss); }
if (*s == '.') { char* e = nullptr; frac = std::strtod(s, &e); if (e != s) s = e; }
return hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL +
static_cast<int64_t>(frac * 1e9);
}
// Days from 1970-01-01 for a proleptic-Gregorian date (Hinnant's algorithm).
inline int64_t days_from_civil(long long y, long long m, long long d) noexcept {
y -= (m <= 2);
const long long era = (y >= 0 ? y : y - 399) / 400;
const long long yoe = y - era * 400;
const long long doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;
const long long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + doe - 719468;
}
inline int64_t parse_date_days(const char* s) noexcept {
s = skip_literal_prefix(s);
long long y = 0, mo = 0, d = 0;
read_int(s, y);
if (*s == '-') { ++s; read_int(s, mo); }
if (*s == '-') { ++s; read_int(s, d); }
return days_from_civil(y, mo ? mo : 1, d ? d : 1);
}
inline int64_t parse_dt_ns(const char* s) noexcept {
s = skip_literal_prefix(s);
long long y = 0, mo = 0, d = 0, hh = 0, mm = 0, ss = 0;
read_int(s, y);
if (*s == '-') { ++s; read_int(s, mo); }
if (*s == '-') { ++s; read_int(s, d); }
if (*s == '-') { ++s; read_int(s, hh); }
if (*s == ':') { ++s; read_int(s, mm); }
if (*s == ':') { ++s; read_int(s, ss); }
return days_from_civil(y, mo ? mo : 1, d ? d : 1) * 86400000000000LL +
hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL;
}
} // namespace iec_strparse
// String overloads (more specialized than the numeric TO_* templates, so they
// win overload resolution for STRING arguments).
template<size_t N> inline IEC_TIME TO_TIME(const IECString<N>& s) noexcept { return IEC_TIME(iec_strparse::parse_time_ns(s.c_str())); }
template<size_t N> inline IEC_TIME TO_TIME(const IECStringVar<N>& s) noexcept { return IEC_TIME(iec_strparse::parse_time_ns(s.get().c_str())); }
template<size_t N> inline IEC_TOD TO_TOD(const IECString<N>& s) noexcept { return IEC_TOD(iec_strparse::parse_tod_ns(s.c_str())); }
template<size_t N> inline IEC_TOD TO_TOD(const IECStringVar<N>& s) noexcept { return IEC_TOD(iec_strparse::parse_tod_ns(s.get().c_str())); }
template<size_t N> inline IEC_DATE TO_DATE(const IECString<N>& s) noexcept { return IEC_DATE(iec_strparse::parse_date_days(s.c_str())); }
template<size_t N> inline IEC_DATE TO_DATE(const IECStringVar<N>& s) noexcept { return IEC_DATE(iec_strparse::parse_date_days(s.get().c_str())); }
template<size_t N> inline IEC_DT TO_DT(const IECString<N>& s) noexcept { return IEC_DT(iec_strparse::parse_dt_ns(s.c_str())); }
template<size_t N> inline IEC_DT TO_DT(const IECStringVar<N>& s) noexcept { return IEC_DT(iec_strparse::parse_dt_ns(s.get().c_str())); }
// =============================================================================
// String / Wide String Conversion
// =============================================================================
//
// STRING ↔ WSTRING per IEC 61131-3 §6.5.4.6: codepoint-by-codepoint
// transcoding. Anything outside the BMP would require surrogate
// handling that the runtime does not implement; OpenPLC programs in
// practice deal in 7-bit ASCII or simple Latin-1, so a lossy narrow
// (truncate the high byte) is documented behaviour rather than a
// surprise. Callers that need full Unicode round-tripping should keep
// data in WSTRING throughout.
template<size_t SrcLen>
inline IECWString<SrcLen> STRING_TO_WSTRING(const IECString<SrcLen>& src) noexcept {
IECWString<SrcLen> result;
const size_t n = src.length();
for (size_t i = 0; i < n; ++i) {
// Treat each STRING byte as a codepoint in the U+0000U+00FF
// range. Multi-byte UTF-8 sequences pass through byte-for-byte
// and end up as Latin-1 — wrong for non-ASCII, but the IEC
// standard doesn't define UTF-8/UTF-16 transcoding either.
result.append(static_cast<char16_t>(static_cast<unsigned char>(src[i])));
}
return result;
}
// Overload for the per-variable wrapper (handles auto-unwrap).
template<size_t SrcLen>
inline IECWString<SrcLen> STRING_TO_WSTRING(const IECStringVar<SrcLen>& src) noexcept {
return STRING_TO_WSTRING(iec_unwrap(src));
}
template<size_t SrcLen>
inline IECString<SrcLen> WSTRING_TO_STRING(const IECWString<SrcLen>& src) noexcept {
IECString<SrcLen> result;
const size_t n = src.length();
for (size_t i = 0; i < n; ++i) {
// Truncate to the low byte. Codepoints > U+00FF lose
// information; surrogate pairs (rare in IEC programs) collapse
// to garbage. Document as "ASCII / Latin-1 only" round-trip.
result.append(static_cast<char>(src[i] & 0xFF));
}
return result;
}
template<size_t SrcLen>
inline IECString<SrcLen> WSTRING_TO_STRING(const IECWStringVar<SrcLen>& src) noexcept {
return WSTRING_TO_STRING(iec_unwrap(src));
}
// `*_TO_*` resolution in the frontend collapses STRING_TO_WSTRING /
// WSTRING_TO_STRING to plain TO_WSTRING / TO_STRING calls (cppName is
// `TO_${toType}`), so provide the matching aliases. Templated on the
// source type so they bind to either the bare class or the *Var
// wrapper without relying on conversions.
template<typename T>
inline auto TO_WSTRING(const T& src) noexcept -> decltype(STRING_TO_WSTRING(src)) {
return STRING_TO_WSTRING(src);
}
template<typename T>
inline auto TO_STRING(const T& src) noexcept -> decltype(WSTRING_TO_STRING(src)) {
return WSTRING_TO_STRING(src);
}
// =============================================================================
// WSTRING → Numeric Conversions
// =============================================================================
//
// IEC 61131-3: WSTRING_TO_INT / WSTRING_TO_REAL / etc. all route through
// WSTRING_TO_STRING (lossy narrow-to-ASCII; same caveat the standard
// transcoding helpers document) and then reuse the STRING parsers
// already defined in iec_string.hpp. This keeps the parsing semantics
// (strtoul / strtol / strtod) byte-identical between the STRING and
// WSTRING surfaces, and the narrow conversion is correct for the
// numeric ASCII / Latin-1 subset users actually write into STRING
// literals.
template<size_t N>
inline IEC_BOOL TO_BOOL(const IECWString<N>& s) noexcept {
return TO_BOOL(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_BOOL TO_BOOL(const IECWStringVar<N>& s) noexcept {
return TO_BOOL(s.get());
}
template<size_t N>
inline IEC_SINT TO_SINT(const IECWString<N>& s) noexcept {
return TO_SINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_SINT TO_SINT(const IECWStringVar<N>& s) noexcept {
return TO_SINT(s.get());
}
template<size_t N>
inline IEC_INT TO_INT(const IECWString<N>& s) noexcept {
return TO_INT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_INT TO_INT(const IECWStringVar<N>& s) noexcept {
return TO_INT(s.get());
}
template<size_t N>
inline IEC_DINT TO_DINT(const IECWString<N>& s) noexcept {
return TO_DINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_DINT TO_DINT(const IECWStringVar<N>& s) noexcept {
return TO_DINT(s.get());
}
template<size_t N>
inline IEC_LINT TO_LINT(const IECWString<N>& s) noexcept {
return TO_LINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_LINT TO_LINT(const IECWStringVar<N>& s) noexcept {
return TO_LINT(s.get());
}
template<size_t N>
inline IEC_USINT TO_USINT(const IECWString<N>& s) noexcept {
return TO_USINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_USINT TO_USINT(const IECWStringVar<N>& s) noexcept {
return TO_USINT(s.get());
}
template<size_t N>
inline IEC_UINT TO_UINT(const IECWString<N>& s) noexcept {
return TO_UINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_UINT TO_UINT(const IECWStringVar<N>& s) noexcept {
return TO_UINT(s.get());
}
template<size_t N>
inline IEC_UDINT TO_UDINT(const IECWString<N>& s) noexcept {
return TO_UDINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_UDINT TO_UDINT(const IECWStringVar<N>& s) noexcept {
return TO_UDINT(s.get());
}
template<size_t N>
inline IEC_ULINT TO_ULINT(const IECWString<N>& s) noexcept {
return TO_ULINT(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_ULINT TO_ULINT(const IECWStringVar<N>& s) noexcept {
return TO_ULINT(s.get());
}
template<size_t N>
inline IEC_REAL TO_REAL(const IECWString<N>& s) noexcept {
return TO_REAL(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_REAL TO_REAL(const IECWStringVar<N>& s) noexcept {
return TO_REAL(s.get());
}
template<size_t N>
inline IEC_LREAL TO_LREAL(const IECWString<N>& s) noexcept {
return TO_LREAL(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_LREAL TO_LREAL(const IECWStringVar<N>& s) noexcept {
return TO_LREAL(s.get());
}
template<size_t N>
inline IEC_BYTE TO_BYTE(const IECWString<N>& s) noexcept {
return TO_BYTE(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_BYTE TO_BYTE(const IECWStringVar<N>& s) noexcept {
return TO_BYTE(s.get());
}
template<size_t N>
inline IEC_WORD TO_WORD(const IECWString<N>& s) noexcept {
return TO_WORD(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_WORD TO_WORD(const IECWStringVar<N>& s) noexcept {
return TO_WORD(s.get());
}
template<size_t N>
inline IEC_DWORD TO_DWORD(const IECWString<N>& s) noexcept {
return TO_DWORD(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_DWORD TO_DWORD(const IECWStringVar<N>& s) noexcept {
return TO_DWORD(s.get());
}
template<size_t N>
inline IEC_LWORD TO_LWORD(const IECWString<N>& s) noexcept {
return TO_LWORD(WSTRING_TO_STRING(s));
}
template<size_t N>
inline IEC_LWORD TO_LWORD(const IECWStringVar<N>& s) noexcept {
return TO_LWORD(s.get());
}
// =============================================================================
// Time Utilities
// =============================================================================
/**
* Create a TIME value from milliseconds
*/
inline IEC_TIME TIME_FROM_MS(int64_t ms) noexcept {
return IEC_TIME(ms * 1000000); // Convert to nanoseconds
}
/**
* Create a TIME value from seconds
*/
inline IEC_TIME TIME_FROM_S(double s) noexcept {
return IEC_TIME(static_cast<int64_t>(s * 1000000000.0));
}
/**
* Get milliseconds from a TIME value
*/
inline int64_t TIME_TO_MS(IEC_TIME t) noexcept {
return iec_unwrap(t) / 1000000;
}
/**
* Get seconds from a TIME value
*/
inline double TIME_TO_S(IEC_TIME t) noexcept {
return static_cast<double>(iec_unwrap(t)) / 1000000000.0;
}
// =============================================================================
// Variadic Arithmetic Functions (ANY_NUM -> ANY_NUM)
// =============================================================================
/**
* NEG - Negation (unary minus)
* Input: ANY_NUM, Output: ANY_NUM (same type)
*/
template<typename T, enable_if_any_num<T> = 0>
inline T NEG(T value) noexcept {
return T(-iec_unwrap(value));
}
/**
* ADD - Addition (variadic)
* Input: ANY_NUM, Output: ANY_NUM (same type)
* Adds two or more values together
*/
template<typename T, enable_if_any_num<T> = 0>
inline T ADD(T a, T b) noexcept {
return T(iec_unwrap(a) + iec_unwrap(b));
}
template<typename T, typename... Args, enable_if_any_num<T> = 0>
inline T ADD(T first, T second, Args... rest) noexcept {
return ADD(T(iec_unwrap(first) + iec_unwrap(second)), rest...);
}
/**
* MUL - Multiplication (variadic)
* Input: ANY_NUM, Output: ANY_NUM (same type)
* Multiplies two or more values together
*/
template<typename T, enable_if_any_num<T> = 0>
inline T MUL(T a, T b) noexcept {
return T(iec_unwrap(a) * iec_unwrap(b));
}
template<typename T, typename... Args, enable_if_any_num<T> = 0>
inline T MUL(T first, T second, Args... rest) noexcept {
return MUL(T(iec_unwrap(first) * iec_unwrap(second)), rest...);
}
/**
* SUB - Subtraction
* Input: ANY_NUM, Output: ANY_NUM (same type)
* Subtracts second value from first
*/
template<typename T, enable_if_any_num<T> = 0>
inline T SUB(T a, T b) noexcept {
return T(iec_unwrap(a) - iec_unwrap(b));
}
/**
* DIV - Division
* Input: ANY_NUM, Output: ANY_NUM (same type)
* Divides first value by second
*/
template<typename T, enable_if_any_num<T> = 0>
inline T DIV(T a, T b) noexcept {
return T(iec_unwrap(a) / iec_unwrap(b));
}
/**
* MOD - Modulo
* Input: ANY_NUM, Output: ANY_NUM (same type)
* Returns remainder of division
*/
template<typename T, enable_if_any_num<T> = 0>
inline T MOD(T a, T b) noexcept {
if constexpr (std::is_floating_point_v<iec_underlying_type_t<T>>) {
return T(std::fmod(static_cast<double>(iec_unwrap(a)), static_cast<double>(iec_unwrap(b))));
} else {
return T(iec_unwrap(a) % iec_unwrap(b));
}
}
// =============================================================================
// Variadic Bitwise Functions (ANY_BIT -> ANY_BIT)
// =============================================================================
/**
* NOT - Bitwise NOT (one's complement)
* Input: ANY_BIT, Output: ANY_BIT (same type)
*
* BOOL needs logical negation, not bitwise: `~bool(true)` integer-promotes
* to `~1 == -2`, and converting back via `bool(-2)` is `true` (any non-zero
* is true), so the bitwise path returns `true` for both inputs. The
* IEC_BOOL specialization handles wrapped booleans, but expressions like
* `NOT(a == b)` instantiate the primary template with `T = bool` (raw)
* because IECVar's comparison operators return plain `bool`. Add a
* raw-bool specialization that uses `!` so NOT(comparison) works.
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T NOT(T value) noexcept {
return T(~iec_unwrap(value));
}
template<>
inline bool NOT(bool value) noexcept {
return !value;
}
template<>
inline IEC_BOOL NOT(IEC_BOOL value) noexcept {
return IEC_BOOL(!iec_unwrap(value));
}
/**
* AND - Bitwise AND (variadic)
* Input: ANY_BIT, Output: ANY_BIT (same type)
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T AND(T a, T b) noexcept {
return T(iec_unwrap(a) & iec_unwrap(b));
}
template<typename T, typename... Args, enable_if_any_bit<T> = 0>
inline T AND(T first, T second, Args... rest) noexcept {
return AND(T(iec_unwrap(first) & iec_unwrap(second)), rest...);
}
/**
* OR - Bitwise OR (variadic)
* Input: ANY_BIT, Output: ANY_BIT (same type)
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T OR(T a, T b) noexcept {
return T(iec_unwrap(a) | iec_unwrap(b));
}
template<typename T, typename... Args, enable_if_any_bit<T> = 0>
inline T OR(T first, T second, Args... rest) noexcept {
return OR(T(iec_unwrap(first) | iec_unwrap(second)), rest...);
}
/**
* XOR - Bitwise XOR (variadic)
* Input: ANY_BIT, Output: ANY_BIT (same type)
*/
template<typename T, enable_if_any_bit<T> = 0>
inline T XOR(T a, T b) noexcept {
return T(iec_unwrap(a) ^ iec_unwrap(b));
}
template<typename T, typename... Args, enable_if_any_bit<T> = 0>
inline T XOR(T first, T second, Args... rest) noexcept {
return XOR(T(iec_unwrap(first) ^ iec_unwrap(second)), rest...);
}
// =============================================================================
// Variadic Selection Functions (ANY_ELEMENTARY)
// =============================================================================
/**
* MAX - Maximum (variadic)
* Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type)
* Returns the maximum of two or more values
*/
template<typename T, typename... Args, enable_if_any_elementary<T> = 0>
inline T MAX(T first, T second, Args... rest) noexcept {
T current_max = iec_unwrap(first) > iec_unwrap(second) ? first : second;
if constexpr (sizeof...(rest) > 0) {
return MAX(current_max, rest...);
} else {
return current_max;
}
}
/**
* MIN - Minimum (variadic)
* Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type)
* Returns the minimum of two or more values
*/
template<typename T, typename... Args, enable_if_any_elementary<T> = 0>
inline T MIN(T first, T second, Args... rest) noexcept {
T current_min = iec_unwrap(first) < iec_unwrap(second) ? first : second;
if constexpr (sizeof...(rest) > 0) {
return MIN(current_min, rest...);
} else {
return current_min;
}
}
/**
* MOVE - Copy value (identity function)
* Input: ANY, Output: ANY (same type)
* Used for explicit value copying in ST
*/
template<typename T>
inline T MOVE(T value) noexcept {
return value;
}
// =============================================================================
// Scan-Cycle Time (CODESYS/MatIEC-compatible)
// =============================================================================
/**
* Global scan-cycle time in nanoseconds.
* Advanced by the runtime before each scan cycle.
* - REPL advances by common_ticktime each cycle.
* - OpenPLC runtime advances before each task execution.
* - Test runner resets to 0 before each test case.
*
* All calls to TIME() within the same cycle return the same value,
* matching CODESYS behavior.
*/
#ifdef STRUCPP_THREADED
// Threaded runtime (OpenPLC v4): each IEC task runs on its own thread and must
// observe an IEC TIME() value that is STABLE for the duration of its scan and
// equal to the time at which the dispatcher released it. thread_local gives
// every worker its own TIME() base; the runtime stamps it via
// strucpp_set_current_time() at each dispatch, so a slow/overrunning task keeps
// reading its own snapshot while the dispatcher's master clock advances freely
// for the other tasks. Gated on STRUCPP_THREADED because single-threaded
// targets (Arduino/bare-metal) may have no TLS runtime — there it stays a plain
// global, which is correct for a one-thread scan loop.
inline thread_local int64_t __CURRENT_TIME_NS = 0;
#else
inline int64_t __CURRENT_TIME_NS = 0;
#endif
/**
* Returns the current scan-cycle time.
* CODESYS-compatible: TIME() returns the same value for the entire cycle.
*/
inline IEC_TIME TIME() {
return IEC_TIME(static_cast<TIME_t>(__CURRENT_TIME_NS));
}
/**
* Wall-clock date-and-time override slot, in nanoseconds since the
* Unix epoch.
*
* Platform integrations that *can* deliver real wall-clock time
* (VPP packages with a DS3231 RTC chip wired up, Wi-Fi targets that
* pull NTP, etc.) populate this before each scan and CURRENT_DT()
* returns it verbatim. Targets without that capability leave it at 0
* and CURRENT_DT() falls back to a meaningful-but-not-wall-clock
* value — see the function comment for the full priority order.
*/
inline int64_t __CURRENT_DT_NS = 0;
/**
* CURRENT_DT() — wall-clock date-and-time.
*
* Returns the current absolute time as IEC_DT (nanoseconds since the
* Unix epoch). Distinct from TIME() which returns the scan-cycle's
* monotonic elapsed time, not a date.
*
* Used by the Additional Function Blocks library's RTC FB, which under
* MatIEC consumed a `__CURRENT_TIME` global the runtime injected before
* each scan. STruC++ exposes the same capability through this regular
* function so RTC's body can call it without compiler-specific pragmas.
*
* Resolution priority (highest first):
* 1. `__CURRENT_DT_NS` when non-zero — the platform integration
* delivered a real wall-clock value (RTC chip, NTP, host syscall
* wired by an OpenPLC v4 runtime, etc.). Honoured on every
* target.
* 2. std::chrono::system_clock on hosted targets — covers REPL, test
* runner, and any g++ build that didn't populate
* `__CURRENT_DT_NS`. Inherits CLOCK_REALTIME's quirks (can step
* backwards if the system clock is corrected); code needing
* strict monotonicity should use TIME() instead.
* 3. `__CURRENT_TIME_NS` (time since program start) on bare-metal
* targets where std::chrono::system_clock isn't available.
* avr-gcc's libstdc++ ships `<chrono>` but omits `system_clock`,
* so we can't reach for it on Arduino / AVR. Returning uptime
* keeps the IEC_DT value monotonically advancing — programs that
* diff two CURRENT_DT() readings still see meaningful elapsed
* time, just expressed in seconds-since-boot rather than seconds-
* since-1970.
*
* VPP packages targeting hardware with an RTC override (1) by writing
* `__CURRENT_DT_NS` from their platform glue. Nothing else in the
* runtime needs to change to enable that path.
*/
inline IEC_DT CURRENT_DT() {
if (__CURRENT_DT_NS != 0) {
return IEC_DT(static_cast<DT_t>(__CURRENT_DT_NS));
}
#ifdef __AVR__
// No system_clock on avr-gcc. `__CURRENT_TIME_NS` advances
// monotonically as the runtime drives the scan cycle, giving us
// time-since-boot — meaningful for diffing timestamps even when
// no RTC is wired up.
return IEC_DT(static_cast<DT_t>(__CURRENT_TIME_NS));
#else
using namespace std::chrono;
auto now = system_clock::now();
auto ns = duration_cast<nanoseconds>(now.time_since_epoch()).count();
return IEC_DT(static_cast<DT_t>(ns));
#endif
}
// =============================================================================
// CODESYS System Functions
// =============================================================================
/**
* ADR(variable) - Returns the memory address of a variable.
* CODESYS extension. Maps to address-of in C++, returning uintptr_t
* for compatibility with pointer arithmetic.
*/
template<typename T>
inline IEC_ULINT ADR(T& var) {
return static_cast<IEC_ULINT>(reinterpret_cast<std::uintptr_t>(&var));
}
/**
* IEC_SIZEOF(var) - Returns the logical IEC type size in bytes.
* For IECVar<T> types, returns sizeof(T) (the underlying type),
* not sizeof(IECVar<T>) which includes the forcing wrapper overhead.
* Matches CODESYS SIZEOF behavior: SIZEOF(INT) = 2, SIZEOF(DINT) = 4, etc.
*/
template<typename T>
inline IEC_UDINT IEC_SIZEOF(const IECVar<T>&) noexcept {
return static_cast<IEC_UDINT>(sizeof(T));
}
template<typename T>
inline IEC_UDINT IEC_SIZEOF(const T&) noexcept {
return static_cast<IEC_UDINT>(sizeof(T));
}
/**
* MEMCPY(dest, src, n) - Copies n bytes from src to dest.
* CODESYS extension. Accepts uintptr_t addresses from ADR() for
* pointer arithmetic compatibility.
*/
inline IEC_ULINT MEMCPY(IEC_ULINT dest, IEC_ULINT src, std::size_t n) {
std::memcpy(reinterpret_cast<void*>(static_cast<std::uintptr_t>(dest)),
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(src)), n);
return dest;
}
} // namespace strucpp