st2cpp Released
A transpiler that compiles IEC 61131-3 Structured Text into clean, native C++17. No virtual machine, no interpreter β just standard C++ you can compile, debug, and integrate anywhere.
Overview
st2cpp is the compiler component of the undoRT ecosystem.
It takes .st source files as input and produces .hpp / .cpp pairs
that can be compiled with any C++17-capable toolchain.
Function Blocks become C++ structs. Programs become classes with a run() method.
The output is readable and auditable β not obfuscated machine code.
- All IEC 61131-3 elementary types
- STRUCT, ENUM, arrays, pointers
- FUNCTION, FUNCTION_BLOCK, PROGRAM
- FB inheritance with SUPER^ access
- Named and positional call syntax
- VAR_OUTPUT binding via =>
- Modular project generation
- Automatic dependency sorting
- Method visibility (PRIVATE, PROTECTED, PUBLIC)
- Header-only runtime (zero deps)
- Single binary, no install required
ProcessImage and IoBus for I/O management.
Installation
Download pre-built binary
Grab the latest release from GitHub Releases:
| Platform | File |
|---|---|
| Linux x64 | st2cpp-linux-x64.tar.gz |
| Linux ARM64 | st2cpp-linux-arm64.tar.gz |
| macOS ARM64 | st2cpp-macos-arm64.tar.gz |
| Windows x64 | st2cpp-win-x64.zip |
# Linux / macOS
tar -xzf st2cpp-*.tar.gz
./st2cpp --help
# Windows
unzip st2cpp-win-x64.zip
st2cpp.exe --help
Build from source
Prerequisites: GCC 7+ / Clang 5+ / MSVC 2017+, CMake 3.16+
git clone https://github.com/undoRT/st2cpp.git
cd st2cpp
./build.sh
# Or manually:
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
Quick Start
# Transpile a single file
./st2cpp program.st --namespace myplc
# Transpile an entire workspace in project mode
./st2cpp --workspace ./src --project-style --output-dir build --namespace myplc
# Compile the generated C++
g++ -std=c++17 -I./st2cpp_includes build/Programs/Main.cpp -o plc_main
CLI Reference
| Option | Description | Default |
|---|---|---|
-o <file> | Output .cpp source file | <input>.cpp |
-H <file> | Output .hpp header file | <input>.hpp |
--namespace <name> | C++ namespace for generated code | st2cpp |
--runtime <file> | Custom runtime header | st2cpp_types.hpp |
--workspace <path> | Process all .st files recursively | β |
--project-style | Modular output with dependency management | off |
--output-dir <dir> | Output directory (workspace/project mode) | generated/ |
--tokens | Dump token stream and exit (debug) | β |
-h, --help | Show help message | β |
Single File Mode
The default mode. Produces one .hpp and one .cpp from a single .st source.
Best for small projects or quick experiments.
./st2cpp counter.st -o counter.cpp -H counter.hpp --namespace myplc
Include st2cpp_types.hpp (from the st2cpp_includes/ folder) in your compiler's
include path when building the output:
g++ -std=c++17 -I./st2cpp_includes counter.cpp -o counter
Project Mode
For workspaces with multiple POUs. st2cpp scans all .st files recursively,
resolves dependencies between Function Blocks via topological sort, and generates a
circular-dependency-safe header layout:
./st2cpp --workspace ./plc_src --project-style --output-dir build --namespace myplc
build/
βββ SimpleGVLs.hpp # ENUMs, simple STRUCTs, simple globals
βββ FunctionBlocks.hpp # Aggregator β includes all FB headers
βββ FunctionBlocks/ # One .hpp/.cpp per Function Block
β βββ Timer.hpp / .cpp
β βββ Counter.hpp / .cpp
βββ GVLs.hpp / .cpp # Complex globals (STRUCTs containing FBs)
βββ Functions.hpp / .cpp # Global functions
βββ Programs/
βββ Main.hpp / .cpp
Full Example
Input (counter.st)
FUNCTION_BLOCK Counter
VAR_INPUT
Enable : BOOL;
Reset : BOOL;
END_VAR
VAR_OUTPUT
Count : INT;
END_VAR
IF Reset THEN
Count := 0;
ELSIF Enable THEN
Count := Count + 1;
END_IF
END_FUNCTION_BLOCK
PROGRAM Main
VAR
C : Counter;
END_VAR
C(Enable := TRUE, Reset := FALSE);
END_PROGRAM
Generated header (counter.hpp)
#pragma once
#include "st2cpp_types.hpp"
namespace myplc {
// FUNCTION BLOCK COUNTER
struct COUNTER {
public:
// VAR_INPUT
Bool ENABLE{};
Bool RESET{};
// VAR_OUTPUT
Int16 COUNT{};
// SETTER/GETTER
inline void set_ENABLE(Bool val) { ENABLE = val; }
inline void set_RESET(Bool val) { RESET = val; }
inline Int16 get_COUNT() const { return COUNT; }
// End SETTER/GETTER
COUNTER();
void operator()();
virtual ~COUNTER() = default;
};
// PROGRAM MAIN
struct MAIN {
COUNTER C{};
void run();
};
} // namespace myplc
Generated source (counter.cpp)
#include "counter.hpp"
namespace myplc {
// FUNCTION BLOCK COUNTER
COUNTER::COUNTER() {}
void COUNTER::operator()() {
if (RESET) {
COUNT = 0;
} else if (ENABLE) {
COUNT = COUNT + 1;
}
}
// END FUNCTION BLOCK COUNTER
void MAIN::run() {
C.set_ENABLE(true);
C.set_RESET(false);
C();
}
} // namespace myplc
Supported Constructs
| Category | Constructs |
|---|---|
| POUs | FUNCTION, FUNCTION_BLOCK, PROGRAM |
| Variable sections | VAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, VAR_EXTERNAL, VAR_GLOBAL |
| Data types | All IEC 61131-3 elementary types (BOOL, INT, DINT, REAL, LREAL, STRING, TIME, β¦) |
| User-defined types | STRUCT β¦ END_STRUCT, ENUM β¦ END_ENUM via TYPE β¦ END_TYPE |
| Arrays | Single and multi-dimensional; bounds-checked via STArray<T,Low,High> (from undoCore) |
| Pointers | POINTER TO, REF_TO |
| Control flow | IF/ELSIF/ELSE, FOR/TO/BY/DO, WHILE/DO, REPEAT/UNTIL, CASE/OF |
| Calls | Positional, named (:=), output binding (=>), IN_OUT reference |
| Inheritance | FUNCTION_BLOCK B EXTENDS A with SUPER^ access |
| Method visibility | METHOD PRIVATE, METHOD PROTECTED, METHOD PUBLIC |
| Initializers | Variable initializers, array literal initializers |
| Typed literals | UDINT#123, ULINT#16#9E3779B9, LREAL#3.14159 |
SIZEOF | SIZEOF(type) and SIZEOF(expression) (extension) |
Type Mapping
st2cpp uses undoCore for type definitions, ensuring consistent type mapping across the entire undoRT ecosystem.
| ST Type | C++ Type | Notes |
|---|---|---|
BOOL | bool | |
SINT | int8_t | |
INT | int16_t | |
DINT | int32_t | |
LINT | int64_t | |
USINT | uint8_t | |
UINT | uint16_t | |
UDINT | uint32_t | |
ULINT | uint64_t | |
REAL | float | |
LREAL | double | |
STRING | std::string | |
TIME | uint32_t | milliseconds |
ARRAY[L..H] OF T | STArray<T, L, H> | bounds-checked (from undoCore) |
POINTER TO T | T* | |
VAR_IN_OUT T | VAR_INOUT<T> (reference wrapper) | from undoCore |
Function Call Translation
| ST Syntax | C++ Output |
|---|---|
foo(10, FALSE) | Positional arguments mapped in declaration order |
foo(a := 1, b := 2) | Named arguments reordered to match declaration |
foo(out => myVar) | Output bound as &myVar, unbound outputs get a temp variable |
foo(inout := myVar) | IN_OUT bound as reference |
FB.method() | Method call on FB struct member |
SUPER^.method() | Base class method call via C++ inheritance |
Method Visibility (New in v0.2.1)
st2cpp now supports PRIVATE, PROTECTED, and PUBLIC visibility
modifiers for methods inside FUNCTION_BLOCK.
ST Syntax
FUNCTION_BLOCK MyFB
VAR
counter : INT;
END_VAR
METHOD PRIVATE splitMix : UDINT
VAR_IN_OUT state : UDINT; END_VAR
// ...
END_METHOD
METHOD PROTECTED init : BOOL
// ...
END_METHOD
METHOD PUBLIC exec : UDINT
VAR_INPUT N : UDINT; END_VAR
// ...
END_METHOD
END_FUNCTION_BLOCK
Generated C++
struct MYFB {
public:
// Variables...
// SETTER/GETTER...
// --- PRIVATE METHODS ---
private:
virtual UInt32 SPLITMIX(UInt32& STATE);
// --- PROTECTED METHODS ---
protected:
virtual Bool INIT();
// --- PUBLIC METHODS ---
public:
MYFB();
void operator()();
virtual UInt32 EXEC(Int16 N);
virtual ~MYFB() = default;
};
PUBLIC.
Constructor, operator(), and destructor are always PUBLIC.
Known Limitations (Beta)
| Limitation | Status |
|---|---|
| Semantic analysis / type checking | In progress |
| Error recovery (first error stops compilation) | Planned v0.3 |
ARRAY[*] variable-length arrays | Planned |
VAR_CONFIG | Planned |
SFC (ACTION, TRANSITION) | Planned |
| IEC standard function library (TON, CTU, ABSβ¦) | Planned v0.3 |
Testing
st2cpp includes a comprehensive test suite built with Google Test. The suite covers every stage of the compiler pipeline and ensures correctness of the generated code. All tests are executed automatically on every commit.
| Test Category | Description | Tests |
|---|---|---|
| Lexer | Tokenization of all IEC 61131β3 tokens (keywords, addresses, literals, operators) | 42 |
| Parser | AST construction for POUs, types, variables, control flow, arrays, interfaces | 30 |
| Code Generator | Translation from AST to C++: functions, FBs, programs, arrays, structs, enums | 35 |
| Address Allocator | Fixed and placeholder address allocation, alignment, region growth | 20 |
| Process Image | Accessor generation for %I, %Q, %M addresses with bit/byte/word/dword access |
20 |
| Scope Manager | Variable lookup, shadowing, functionβlocal flags, temporary counters | 20 |
| Compilation | Compilation of generated C++ code with a real compiler (g++) |
31 |
| Complex Programs | Integration tests with arrays, inheritance, interfaces, SUPER^ |
19 |
| Golden Tests | Comparison against preβapproved output for sample .st files |
10+ |
The test suite is located in the tests/ directory and can be built and run
with make test or ctest after compiling the project.
Architecture
st2cpp follows a classic compiler frontend pipeline. All stages are hand-written in C++17 with no external parser generator dependencies.
ST Source
βββΆ Lexer (Token stream)
βββΆ Parser (Recursive descent β AST)
βββΆ CodeGen (AST visitor β C++17 source)
The AST uses std::variant for node types (ExprVariant, StmtVariant)
with std::visit dispatch in the code generator β no virtual dispatch, no
dynamic_cast.
#include <undoCore/types.hpp> and uses STArray,
VAR_INOUT, and conversion functions from the undoCore library.
Repository Layout
st2cpp/
βββ include/
β βββ ast/ # AST node definitions (std::variant-based)
β βββ lexer/ # Token types and Lexer class
β βββ parser/ # Parser class and helpers
β βββ codegen/ # CodeGenerator class
βββ src/
β βββ lexer/ # Lexer implementation
β βββ parser/ # Parser + statement parser
β βββ codegen/ # Code generator (~3600 lines)
β βββ cli/ # main.cpp + argument parsing
βββ st2cpp_includes/
β βββ undoCore/ # undoCore submodule (types, conversions, math)
βββ tests/ # 217+ tests
βββ scripts/ # build.sh, generate_docs.sh
βββ CMakeLists.txt
Full API documentation is generated with Doxygen: Browse API Reference
st2cpp