Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

STruC++ is an IEC 61131-3 Structured Text to C++17 compiler written in TypeScript. It targets replacing MatIEC in the OpenPLC toolchain with a cleaner, maintainable architecture.
STruC++ is an IEC 61131-3 Structured Text to C++14 compiler written in TypeScript. It targets replacing MatIEC in the OpenPLC toolchain with a cleaner, maintainable architecture.

## Build & Development Commands

Expand All @@ -30,7 +30,7 @@ npm run test:watch # Watch mode

### C++ Compilation Tests

Tests in `tests/integration/cpp-compile.test.ts` require `g++` with C++17 support. They are auto-skipped if g++ is unavailable.
Tests in `tests/integration/cpp-compile.test.ts` require `g++` with C++14 support. They are auto-skipped if g++ is unavailable.

## Architecture

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# STruC++

**IEC 61131-3 Structured Text to C++17 compiler.**
**IEC 61131-3 Structured Text to C++14 compiler.**

[![CI](https://github.com/Autonomy-Logic/STruCpp/actions/workflows/ci.yml/badge.svg)](https://github.com/Autonomy-Logic/STruCpp/actions/workflows/ci.yml)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE)

STruC++ compiles PLC programs written in [Structured Text](https://en.wikipedia.org/wiki/Structured_text) into clean, readable C++17. It ships with a built-in unit testing framework, an interactive REPL for program debugging, and a reusable library system.
STruC++ compiles PLC programs written in [Structured Text](https://en.wikipedia.org/wiki/Structured_text) into clean, readable C++14. It ships with a built-in unit testing framework, an interactive REPL for program debugging, and a reusable library system.

> The name **STruC++** comes from **ST** (Structured Text) + **stru** (Latin root meaning "to build") + **C++** (the target language).

---

## Why STruC++?

**ST-to-C++ makes sense.** Other tools target C (MatIEC) or proprietary bytecode. STruC++ generates idiomatic C++17 with classes for function blocks, virtual methods for interfaces, and templates for generics, producing code you can actually read, debug, and integrate with existing C++ projects.
**ST-to-C++ makes sense.** Other tools target C (MatIEC) or proprietary bytecode. STruC++ generates idiomatic C++14 with classes for function blocks, virtual methods for interfaces, and templates for generics, producing code you can actually read, debug, and integrate with existing C++ projects.

**Built-in unit testing.** Every PLC testing solution today requires a separate IDE add-on, an external library, or PLC hardware to run tests. STruC++ has a test runner built into the compiler itself. Users can write tests in ST, run them on any machine with `strucpp source.st --test tests.st`. No PLC needed, perfect for automated build pipelines. See the [Testing Guide](docs/TESTING.md).

**Interactive REPL.** Build your ST program into a standalone binary with `--build` and step through it interactively to check correctness. The interactive REPL allows users to print ST and C++ code side-by-side, set inputs, advance cycles, inspect variables, and force values. See the [REPL Guide](docs/REPL.md).

**Zero runtime dependencies.** The compiler is a single binary. The C++ runtime is header-only. Generated code compiles with any C++17 compiler (g++, clang++, MSVC). See the [CLI Reference](docs/CLI.md) and [C++ Runtime](docs/RUNTIME.md).
**Zero runtime dependencies.** The compiler is a single binary. The C++ runtime is header-only. Generated code and the runtime compile at C++14, exercised with g++ 7.3 and newer (avr-g++ 7.3 is the oldest toolchain in the board matrix); clang++ and MSVC are not exercised by CI. C++14 rather than C++17 because the generated code is compiled by the Arduino core's own toolchain, and the mbed-based cores hard-code `-std=gnu++14`. See the [CLI Reference](docs/CLI.md) and [C++ Runtime](docs/RUNTIME.md).

---

Expand All @@ -42,7 +42,7 @@ strucpp counter.st -o counter.cpp
This generates `counter.cpp` and `counter.hpp`. To compile the C++ output:

```bash
g++ -std=c++17 -Istrucpp/runtime/include counter.cpp -o counter
g++ -std=c++14 -Istrucpp/runtime/include counter.cpp -o counter
```

### Run Unit Tests
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# STruC++ Architecture

STruC++ is an IEC 61131-3 Structured Text to C++17 compiler written in TypeScript. It produces readable, debuggable C++ output with line mapping back to the original ST source.
STruC++ is an IEC 61131-3 Structured Text to C++14 compiler written in TypeScript. It produces readable, debuggable C++ output with line mapping back to the original ST source.

## Compilation Pipeline

Expand Down
4 changes: 2 additions & 2 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ strucpp/

The `--build` and `--test` modes compile C++ code and require a working toolchain on the host:

- `g++` with C++17 support (GCC 7+ or equivalent)
- `g++` with C++14 support (GCC 5+ or equivalent)
- `cc` (any C11 compiler, for the REPL line editor)

The default compile mode (ST to C++) has no external dependencies.
Expand Down Expand Up @@ -69,7 +69,7 @@ Compile to C++ and produce an interactive REPL binary (see [REPL.md](REPL.md)):
strucpp input.st -o output.cpp --build
```

Requires `g++` (C++17) and `cc` (C11) on the system. The build process:
Requires `g++` (C++14) and `cc` (C11) on the system. The build process:

1. Compiles ST to C++ (`.cpp` + `.hpp`)
2. Generates a REPL `main.cpp` with embedded source, line maps, and program descriptors
Expand Down
2 changes: 1 addition & 1 deletion docs/REPL.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Programs without an explicit interval default to 20ms.

The `--build` flag requires:

- `g++` with C++17 support (customizable with `--gpp <path>`)
- `g++` with C++14 support (customizable with `--gpp <path>`)
- A C compiler (`cc` or `gcc`, customizable with `--cc <path>`)

Both are used during the build step only -- the resulting binary is standalone.
2 changes: 1 addition & 1 deletion docs/RUNTIME.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# C++ Runtime Library

The STruC++ runtime is a header-only C++17 library in `src/runtime/include/`. Every compiled ST program includes these headers. The runtime provides IEC 61131-3 type wrappers, variable forcing, and standard function implementations.
The STruC++ runtime is a header-only C++14 library in `src/runtime/include/`. Every compiled ST program includes these headers. The runtime provides IEC 61131-3 type wrappers, variable forcing, and standard function implementations.

## Type Definitions (`iec_types.hpp`)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "strucpp",
"version": "0.6.9",
"version": "0.7.0",
"description": "IEC 61131-3 Structured Text to C++ Compiler",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion src/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
}

// Check scope filter
if (upperScope && nodeScope && nodeScope.toUpperCase() !== upperScope) {

Check warning on line 194 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 194 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
// Still recurse into children — a matching POU may be nested inside
for (const child of getChildren(node)) {
visit(child, nodeScope);
Expand Down Expand Up @@ -327,7 +327,7 @@
column: number,
): boolean {
const span = node.sourceSpan;
if (!span || span.file !== file) return false;

Check warning on line 330 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected object value in conditional. The condition is always true
if (line < span.startLine || line > span.endLine) return false;
if (line === span.startLine && column < span.startCol) return false;
if (line === span.endLine && column > span.endCol) return false;
Expand Down Expand Up @@ -615,13 +615,15 @@

case "VariableExpression": {
const ve = node as VariableExpression;
children.push(...ve.subscripts);
// `accessChain` holds its own clones of the indices — walking both visits each twice.
if (ve.accessChain) {
for (const step of ve.accessChain) {
if (step.kind === "subscript") {
children.push(...step.indices);
}
}
} else {
children.push(...ve.subscripts);
}
break;
}
Expand Down
3 changes: 3 additions & 0 deletions src/backend/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@
if (!match) return null;

const areaChar = match[1]!.toUpperCase();
const sizeChar = match[2]?.toUpperCase() || "X";

Check warning on line 125 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
const byteIndex = parseInt(match[3]!, 10);
const bitIndex = match[4] ? parseInt(match[4], 10) : 0;

Check warning on line 127 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

const areaMap: Record<string, "Input" | "Output" | "Memory"> = {
I: "Input",
Expand Down Expand Up @@ -690,7 +690,7 @@
// Handle inline array types with dimension info
// Array1D stores T directly — use IECVar-wrapped types for elementary elements
// and bare names for composites (whose fields already contain IECVar leaves)
if (typeRef.arrayDimensions && typeRef.elementTypeName) {

Check warning on line 693 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
const elemCpp = this.isUserDefinedType(typeRef.elementTypeName)
? typeRef.elementTypeName
: this.mapVarTypeToCpp(typeRef.elementTypeName);
Expand All @@ -711,7 +711,7 @@
// wrappers all take the raw element type (not IECVar-wrapped); they wrap
// an IECVar<T> internally.
let elemType: string;
if (typeRef.arrayDimensions && typeRef.elementTypeName) {

Check warning on line 714 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
// Array pointer/reference: baseType is already raw (Array1D<...>)
elemType = baseType;
} else if (this.isUserDefinedType(typeRef.name)) {
Expand Down Expand Up @@ -796,14 +796,14 @@
f.type,
);
// Store array metadata for inline array type reconstruction
if (f.arrayDimensions || f.elementTypeName || f.referenceKind) {

Check warning on line 799 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 799 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
const ref: {
arrayDimensions?: Array<{ start: number; end: number }>;
elementTypeName?: string;
referenceKind?: string;
} = {};
if (f.arrayDimensions) ref.arrayDimensions = f.arrayDimensions;
if (f.elementTypeName) ref.elementTypeName = f.elementTypeName;

Check warning on line 806 in src/backend/codegen.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
if (f.referenceKind) ref.referenceKind = f.referenceKind;
this.libraryFBFieldTypeRefs.set(
`${fbUpper}.${f.name.toUpperCase()}`,
Expand Down Expand Up @@ -1259,6 +1259,8 @@
for (const decl of block.declarations) {
const cppType = this.mapTypeRefToCpp(decl.type);
for (const name of decl.names) {
// `inline`, not a static-local behind a reference: the debug table takes
// their address into PROGMEM, which needs a constant.
this.emitHeaderChunkMarker("begin", "inlineGlobal", name);
if (decl.initialValue) {
const initExpr = this.generateInitializer(
Expand Down Expand Up @@ -2742,6 +2744,7 @@
);
emittedAny = true;
}
// Must stay a real object — generated_debug.cpp takes `&name` into PROGMEM.
this.emitHeader(
`inline GlobalVar<${cppType}> ${gvar.name}{${initVal}};`,
);
Expand Down
4 changes: 2 additions & 2 deletions src/backend/struct-init-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
/**
* STruC++ Structure Initializer Code Generation
*
* Lowers IEC 61131-3 `structure_initialization` (Annex B.1.4.3) to C++17.
* Lowers IEC 61131-3 `structure_initialization` (Annex B.1.4.3) to C++14.
*
* p : Point := (y := 2.0, x := 1.0);
*
* strucpp::iec_struct_init<POINT>([](auto& v0) { v0.Y = 2.0; v0.X = 1.0; })
*
* Elements may be written in any order and may be omitted (an omitted element
* keeps the default from its own declaration), which rules out a plain braced
* aggregate initializer — C++17 has no designated initializers. The runtime
* aggregate initializer — C++14 has no designated initializers. The runtime
* helper default-constructs the value and the lambda overwrites exactly the
* elements the initializer names.
*
Expand Down
4 changes: 2 additions & 2 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ function runTestMode(options: CLIOptions): void {
execFileSync(
options.gpp,
[
"-std=c++17",
"-std=c++14",
`-I${runtimeIncludeDir}`,
`-I${testRuntimeDir}`,
`-I${tempDir}`,
Expand Down Expand Up @@ -1168,7 +1168,7 @@ async function main(): Promise<void> {

// Step 2: Compile C++ and link with isocline.o (uses execFileSync to avoid shell injection)
const gppArgs = [
"-std=c++17",
"-std=c++14",
`-I${runtimeIncludeDir}`,
`-I${replDir}`,
`-I${outputDir}`,
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ This directory contains the C++ runtime library for STruC++, providing IEC 61131
### Prerequisites

- CMake 3.14 or higher
- C++17 compatible compiler (GCC 7+, Clang 5+, MSVC 2017+)
- C++14 compatible compiler. Tested with GCC 7.3+ (avr-g++ 7.3 is the oldest
toolchain in the board matrix); Clang and MSVC are not exercised by CI.
- Google Test (automatically downloaded by CMake)

### Build Instructions
Expand Down
13 changes: 9 additions & 4 deletions src/runtime/include/debug_dispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,9 @@ struct PtrOps {

// ---------------------------------------------------------------------------
// type_ops[]: one row per TypeTag, in tag order.
// Kept inline so no separate .cpp is required.
// Header-scope `constexpr`, so no separate .cpp is required.
//
// NOT flash-resident on AVR, whatever "inline constexpr" suggests. The Entry
// NOT flash-resident on AVR, whatever `constexpr` suggests. The Entry
// tables carry STRUCPP_DEBUG_FLASH (see debug_table.hpp) and this does not, so
// on a Harvard target it is const data in .rodata, which the startup code
// copies into SRAM. Every AVR firmware pays for this table.
Expand All @@ -337,8 +337,13 @@ struct PtrOps {
// Moving this one to STRUCPP_DEBUG_FLASH would mean routing every row read
// through pgm_read_ptr in the hot path of handle_read / handle_write /
// handle_set. Worth doing on its own evidence, not as a side effect.
//
// Plain `constexpr`, not `inline constexpr`, because the runtime is C++14:
// that gives the table internal linkage, one copy per translation unit.
// Harmless while a single TU includes this header — include it from a
// second and an AVR pays for the table twice in SRAM.
// ---------------------------------------------------------------------------
inline constexpr TypeOps type_ops[TAG__COUNT] = {
constexpr TypeOps type_ops[TAG__COUNT] = {
/*BOOL */ { &force_impl<BOOL_t>, &unforce_impl<BOOL_t>, &read_impl<BOOL_t>, &write_impl<BOOL_t>, sizeof(BOOL_t) },
/*SINT */ { &force_impl<SINT_t>, &unforce_impl<SINT_t>, &read_impl<SINT_t>, &write_impl<SINT_t>, sizeof(SINT_t) },
/*USINT */ { &force_impl<USINT_t>, &unforce_impl<USINT_t>, &read_impl<USINT_t>, &write_impl<USINT_t>, sizeof(USINT_t) },
Expand Down Expand Up @@ -382,7 +387,7 @@ inline constexpr TypeOps type_ops[TAG__COUNT] = {
//
// So the feature now costs what it costs, and only to firmware that uses it.
// ---------------------------------------------------------------------------
inline constexpr PtrOps ptr_ops[TAG__COUNT] = {
constexpr PtrOps ptr_ops[TAG__COUNT] = {
/*BOOL */ { &ptr_impl<BOOL_t> },
/*SINT */ { &ptr_impl<SINT_t> },
/*USINT */ { &ptr_impl<USINT_t> },
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/include/iec_date.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ inline constexpr int64_t iec_floor_div(int64_t n, int64_t d) noexcept {
return (n % d != 0 && ((n < 0) != (d < 0))) ? q - 1 : q;
}

inline constexpr int64_t DATE_SECONDS_PER_DAY = IEC_SECONDS_PER_DAY;
inline constexpr int64_t DATE_NS_PER_DAY = IEC_NS_PER_DAY;
constexpr int64_t DATE_SECONDS_PER_DAY = IEC_SECONDS_PER_DAY;
constexpr int64_t DATE_NS_PER_DAY = IEC_NS_PER_DAY;

inline int64_t DATE_TO_SECONDS(IEC_DATE d) noexcept {
return iec_unwrap(d) * DATE_SECONDS_PER_DAY;
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/include/iec_dt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ namespace strucpp {
// ---------------------------------------------------------------------------
// Duplicated from iec_time.hpp on purpose: clients sometimes include
// iec_dt.hpp without iec_time.hpp, and the `DT_FROM_*` helpers below
// need the unit factor. C++ tolerates redeclaration of inline
// constexpr at namespace scope as long as the value matches.
inline constexpr int64_t DT_NS_PER_DAY = IEC_NS_PER_DAY;
// need the unit factor. Plain `constexpr` under C++14, so each header
// carries its own internal-linkage copy and the values must agree.
constexpr int64_t DT_NS_PER_DAY = IEC_NS_PER_DAY;

// ---------------------------------------------------------------------------
// Construction helpers
Expand Down
20 changes: 10 additions & 10 deletions src/runtime/include/iec_ptr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,12 @@ class IEC_Ptr {
// Pointer arithmetic (in units of sizeof(T))
// Use templates to directly match any arithmetic type, avoiding
// ambiguity with the integral conversion operator.
template<typename N, std::enable_if_t<std::is_arithmetic_v<N>, int> = 0>
template<typename N, std::enable_if_t<std::is_arithmetic<N>::value, int> = 0>
IEC_Ptr operator+(N n) const noexcept {
return IEC_Ptr(static_cast<T*>(ptr_) + static_cast<std::ptrdiff_t>(n));
}

template<typename N, std::enable_if_t<std::is_arithmetic_v<N>, int> = 0>
template<typename N, std::enable_if_t<std::is_arithmetic<N>::value, int> = 0>
IEC_Ptr operator-(N n) const noexcept {
return IEC_Ptr(static_cast<T*>(ptr_) - static_cast<std::ptrdiff_t>(n));
}
Expand All @@ -149,13 +149,13 @@ class IEC_Ptr {
return IEC_Ptr(static_cast<T*>(ptr_) - static_cast<std::ptrdiff_t>(n.get()));
}

template<typename N, std::enable_if_t<std::is_arithmetic_v<N>, int> = 0>
template<typename N, std::enable_if_t<std::is_arithmetic<N>::value, int> = 0>
IEC_Ptr& operator+=(N n) noexcept {
ptr_ = static_cast<T*>(ptr_) + static_cast<std::ptrdiff_t>(n);
return *this;
}

template<typename N, std::enable_if_t<std::is_arithmetic_v<N>, int> = 0>
template<typename N, std::enable_if_t<std::is_arithmetic<N>::value, int> = 0>
IEC_Ptr& operator-=(N n) noexcept {
ptr_ = static_cast<T*>(ptr_) - static_cast<std::ptrdiff_t>(n);
return *this;
Expand Down Expand Up @@ -222,27 +222,27 @@ class IEC_Ptr {
bool operator!=(std::nullptr_t) const noexcept { return ptr_ != nullptr; }

// Comparison with IECVar<integer> (for CODESYS: PT < DWORD_END)
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator<(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) < static_cast<uintptr_t>(other.get());
}
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator>(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) > static_cast<uintptr_t>(other.get());
}
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator<=(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) <= static_cast<uintptr_t>(other.get());
}
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator>=(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) >= static_cast<uintptr_t>(other.get());
}
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator==(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) == static_cast<uintptr_t>(other.get());
}
template<typename U, std::enable_if_t<std::is_integral_v<U>, int> = 0>
template<typename U, std::enable_if_t<std::is_integral<U>::value, int> = 0>
bool operator!=(IECVar<U> other) const noexcept {
return reinterpret_cast<uintptr_t>(ptr_) != static_cast<uintptr_t>(other.get());
}
Expand Down
Loading
Loading