st2cpp v0.2

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.

πŸ’‘ Integration: The generated C++ code is designed to work seamlessly with undoCore and undoPLC. Use ProcessImage and IoBus for I/O management.

Installation

Download pre-built binary

Grab the latest release from GitHub Releases:

PlatformFile
Linux x64st2cpp-linux-x64.tar.gz
Linux ARM64st2cpp-linux-arm64.tar.gz
macOS ARM64st2cpp-macos-arm64.tar.gz
Windows x64st2cpp-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

OptionDescriptionDefault
-o <file>Output .cpp source file<input>.cpp
-H <file>Output .hpp header file<input>.hpp
--namespace <name>C++ namespace for generated codest2cpp
--runtime <file>Custom runtime headerst2cpp_types.hpp
--workspace <path>Process all .st files recursivelyβ€”
--project-styleModular output with dependency managementoff
--output-dir <dir>Output directory (workspace/project mode)generated/
--tokensDump token stream and exit (debug)β€”
-h, --helpShow 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
Use project mode for any workspace with more than 2–3 Function Blocks. It enables incremental compilation and keeps each FB as a separate file in your IDE.

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

CategoryConstructs
POUsFUNCTION, FUNCTION_BLOCK, PROGRAM
Variable sectionsVAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, VAR_EXTERNAL, VAR_GLOBAL
Data typesAll IEC 61131-3 elementary types (BOOL, INT, DINT, REAL, LREAL, STRING, TIME, …)
User-defined typesSTRUCT … END_STRUCT, ENUM … END_ENUM via TYPE … END_TYPE
ArraysSingle and multi-dimensional; bounds-checked via STArray<T,Low,High> (from undoCore)
PointersPOINTER TO, REF_TO
Control flowIF/ELSIF/ELSE, FOR/TO/BY/DO, WHILE/DO, REPEAT/UNTIL, CASE/OF
CallsPositional, named (:=), output binding (=>), IN_OUT reference
InheritanceFUNCTION_BLOCK B EXTENDS A with SUPER^ access
Method visibilityMETHOD PRIVATE, METHOD PROTECTED, METHOD PUBLIC
InitializersVariable initializers, array literal initializers
Typed literalsUDINT#123, ULINT#16#9E3779B9, LREAL#3.14159
SIZEOFSIZEOF(type) and SIZEOF(expression) (extension)

Type Mapping

st2cpp uses undoCore for type definitions, ensuring consistent type mapping across the entire undoRT ecosystem.

ST TypeC++ TypeNotes
BOOLbool
SINTint8_t
INTint16_t
DINTint32_t
LINTint64_t
USINTuint8_t
UINTuint16_t
UDINTuint32_t
ULINTuint64_t
REALfloat
LREALdouble
STRINGstd::string
TIMEuint32_tmilliseconds
ARRAY[L..H] OF TSTArray<T, L, H>bounds-checked (from undoCore)
POINTER TO TT*
VAR_IN_OUT TVAR_INOUT<T> (reference wrapper)from undoCore

Function Call Translation

ST SyntaxC++ 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;
};
Default visibility: Methods without explicit visibility default to PUBLIC. Constructor, operator(), and destructor are always PUBLIC.

Known Limitations (Beta)

st2cpp is in active development. The following features are not yet supported.
LimitationStatus
Semantic analysis / type checkingIn progress
Error recovery (first error stops compilation)Planned v0.3
ARRAY[*] variable-length arraysPlanned
VAR_CONFIGPlanned
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 CategoryDescriptionTests
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+
βœ… Total: Over 217 tests are run on every commit to ensure reliability and prevent regressions.

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.

πŸ”— Integration with undoCore: The generated code includes #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