From cafa831214866a3c7b22be1ccd2fb562f1f68e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Wed, 16 Sep 2026 17:27:40 -0300 Subject: [PATCH 1/7] fix(lsp): stop emitting overlapping semantic tokens for arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A client that receives overlapping ranges drops tokens and warns, so highlighting silently degraded on any line declaring or indexing an array. Three causes, two of them real defects and one missing guard. `getChildren` pushed a VariableExpression's `subscripts` and the indices carried by its `accessChain`. Those are separate clones of the same indices, so every subscript index was walked twice and emitted twice. This was not only a highlighting problem: references and rename were counting index variables twice as well. `emitTypeReference` painted `name.length` characters from the type's start column, but an inline array's TypeReference is synthesised — `__INLINE_ARRAY_INT`, `__VLA_…` — and carries the whole declaration's span, so it painted 18 characters over the variable name. No token can be correct for a name that is not in the source, so skip them. `deltaEncode` now drops fully covered tokens and clips partial overlaps after the sort, which makes this class of defect unreachable from the client whatever the emitters do. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01P2Wpvri7jZKGPUqgWrpAkE --- src/ast-utils.ts | 4 +- .../server/src/semantic-tokens.ts | 24 ++++- .../unit/semantic-tokens-overlap.test.ts | 102 ++++++++++++++++++ 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 vscode-extension/tests/unit/semantic-tokens-overlap.test.ts diff --git a/src/ast-utils.ts b/src/ast-utils.ts index 84df28d0..1026bbcf 100644 --- a/src/ast-utils.ts +++ b/src/ast-utils.ts @@ -615,13 +615,15 @@ function getChildren(node: ASTNode): ASTNode[] { 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; } diff --git a/vscode-extension/server/src/semantic-tokens.ts b/vscode-extension/server/src/semantic-tokens.ts index fb0a7b07..4c714f54 100644 --- a/vscode-extension/server/src/semantic-tokens.ts +++ b/vscode-extension/server/src/semantic-tokens.ts @@ -290,6 +290,8 @@ function emitTypeReference( if (!tr.sourceSpan) return; // Skip REF_TO/REFERENCE_TO keyword-prefixed references (the type name position is offset) if (tr.isReference) return; + // Synthesised inline-array names (`__INLINE_ARRAY_INT`, `__VLA_…`) have no source text to cover. + if (tr.name.startsWith("__")) return; const upperName = tr.name.toUpperCase(); @@ -389,13 +391,29 @@ function deltaEncode(tokens: RawToken[]): number[] { const data: number[] = []; let prevLine = 0; let prevCol = 0; + // A client given overlapping ranges drops tokens, so clip rather than ship them. + let lastLine = -1; + let lastEnd = 0; for (const t of tokens) { + let col = t.col; + let length = t.length; + + if (t.line === lastLine) { + if (col + length <= lastEnd) continue; + if (col < lastEnd) { + length -= lastEnd - col; + col = lastEnd; + } + } + const deltaLine = t.line - prevLine; - const deltaCol = deltaLine === 0 ? t.col - prevCol : t.col; - data.push(deltaLine, deltaCol, t.length, t.typeIdx, t.modBits); + const deltaCol = deltaLine === 0 ? col - prevCol : col; + data.push(deltaLine, deltaCol, length, t.typeIdx, t.modBits); prevLine = t.line; - prevCol = t.col; + prevCol = col; + lastLine = t.line; + lastEnd = col + length; } return data; diff --git a/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts b/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts new file mode 100644 index 00000000..6913dd31 --- /dev/null +++ b/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 Autonomy / OpenPLC Project +import { describe, it, expect } from "vitest"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { analyze } from "strucpp"; +import { getSemanticTokens, TOKEN_TYPES } from "../../server/src/semantic-tokens.js"; + +interface Tok { + line: number; + col: number; + len: number; + type: number; +} + +function decode(data: number[]): Tok[] { + const out: Tok[] = []; + let line = 0; + let col = 0; + for (let i = 0; i < data.length; i += 5) { + const deltaLine = data[i]; + line += deltaLine; + col = deltaLine === 0 ? col + data[i + 1] : data[i + 1]; + out.push({ line, col, len: data[i + 2], type: data[i + 3] }); + } + return out; +} + +/** The client's rule: within a line no token may start before the previous one ends. */ +function findOverlaps(source: string, fileName: string): string[] { + const data = getSemanticTokens(analyze(source, { fileName }), fileName, source); + const lines = source.split("\n"); + const byLine = new Map(); + for (const t of decode(data)) { + const list = byLine.get(t.line); + if (list) list.push(t); + else byLine.set(t.line, [t]); + } + + const bad: string[] = []; + for (const [line, list] of byLine) { + list.sort((a, b) => a.col - b.col); + for (let i = 1; i < list.length; i++) { + const prev = list[i - 1]; + const cur = list[i]; + if (cur.col < prev.col + prev.len) { + const text = lines[line] ?? ""; + bad.push( + `line ${line + 1} col ${cur.col + 1}: ` + + `"${text.slice(prev.col, prev.col + prev.len)}"(${TOKEN_TYPES[prev.type]}) ` + + `vs "${text.slice(cur.col, cur.col + cur.len)}"(${TOKEN_TYPES[cur.type]})`, + ); + } + } + } + return bad; +} + +function tokensOn(source: string, line: number): Array<{ text: string; type: string }> { + const data = getSemanticTokens(analyze(source, { fileName: "t.st" }), "t.st", source); + const text = source.split("\n")[line - 1] ?? ""; + return decode(data) + .filter((t) => t.line === line - 1) + .sort((a, b) => a.col - b.col) + .map((t) => ({ text: text.slice(t.col, t.col + t.len), type: TOKEN_TYPES[t.type] })); +} + +const CASES: Record = { + "array declaration": `PROGRAM Main\nVAR\n Arr : ARRAY [1..10] OF INT;\nEND_VAR\n ;\nEND_PROGRAM\n`, + "array subscript": `PROGRAM Main\nVAR\n Arr : ARRAY [1..10] OF INT;\n I : INT;\nEND_VAR\n Arr[I] := 0;\nEND_PROGRAM\n`, + "two-dimensional array": `PROGRAM Main\nVAR\n Grid : ARRAY [1..4, 1..4] OF INT;\n I : INT;\nEND_VAR\n Grid[I, I] := 0;\nEND_PROGRAM\n`, + "array of user-defined type": `TYPE\n Motor : STRUCT\n Speed : INT;\n END_STRUCT;\nEND_TYPE\n\nPROGRAM Main\nVAR\n Bank : ARRAY [1..3] OF Motor;\nEND_VAR\n ;\nEND_PROGRAM\n`, + "literal subscript": `PROGRAM Main\nVAR\n Arr : ARRAY [1..10] OF INT;\nEND_VAR\n Arr[1] := 0;\nEND_PROGRAM\n`, + "subscript on the right-hand side": `PROGRAM Main\nVAR\n Arr : ARRAY [1..10] OF INT;\n I : INT;\n X : INT;\nEND_VAR\n X := Arr[I];\nEND_PROGRAM\n`, + "field then subscript": `TYPE\n S : STRUCT\n A : ARRAY [1..3] OF INT;\n END_STRUCT;\nEND_TYPE\n\nPROGRAM Main\nVAR\n X : S;\n I : INT;\nEND_VAR\n X.A[I] := 0;\nEND_PROGRAM\n`, + "no array": `PROGRAM Main\nVAR\n I : INT;\n X : INT;\nEND_VAR\n X := I;\nEND_PROGRAM\n`, +}; + +describe("semantic tokens never overlap", () => { + for (const [name, source] of Object.entries(CASES)) { + it(name, () => { + expect(findOverlaps(source, `${name}.st`)).toEqual([]); + }); + } + + for (const fixture of ["complex-project.st", "simple-program.st"]) { + it(`fixture ${fixture}`, () => { + const file = path.resolve(__dirname, "../fixtures", fixture); + expect(findOverlaps(fs.readFileSync(file, "utf-8"), fixture)).toEqual([]); + }); + } + + it("keeps the variable name a variable on an array declaration", () => { + // The synthesised array type has no source text, so the name is the line's only token. + expect(tokensOn(CASES["array declaration"], 3)).toEqual([{ text: "Arr", type: "variable" }]); + }); + + it("emits a subscript index once", () => { + const onBody = tokensOn(CASES["array subscript"], 6); + expect(onBody.filter((t) => t.text === "I")).toHaveLength(1); + }); +}); From e4d9b234c36ca03a957d6b96f90703c5912d08d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Fri, 18 Sep 2026 11:23:56 -0300 Subject: [PATCH 2/7] fix(lsp): narrow the synthetic-type guard to the inline-array prefixes `startsWith("__")` suppressed more than the synthesised names it was meant for. `__XWORD` is a real elementary type, so `addr : __XWORD;` lost its type token entirely, and any user type whose name opens with underscores lost its highlighting the same way. Match the two prefixes the AST builder actually mints instead, and cover both cases with a test. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jjy1Ja62BFtay579KYhBC7 --- vscode-extension/server/src/semantic-tokens.ts | 8 ++++++-- .../tests/unit/semantic-tokens-overlap.test.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/vscode-extension/server/src/semantic-tokens.ts b/vscode-extension/server/src/semantic-tokens.ts index 4c714f54..aedaa5b0 100644 --- a/vscode-extension/server/src/semantic-tokens.ts +++ b/vscode-extension/server/src/semantic-tokens.ts @@ -59,6 +59,9 @@ export const TOKEN_MODIFIERS: string[] = [ const TYPE_IDX = Object.fromEntries(TOKEN_TYPES.map((t, i) => [t, i])); const MOD_BIT = Object.fromEntries(TOKEN_MODIFIERS.map((m, i) => [m, 1 << i])); +/** Names the AST builder mints for inline arrays; they appear in no source file. */ +const SYNTHETIC_TYPE_PREFIXES = ["__INLINE_ARRAY_", "__VLA_"]; + // --------------------------------------------------------------------------- // Raw token collection // --------------------------------------------------------------------------- @@ -290,8 +293,9 @@ function emitTypeReference( if (!tr.sourceSpan) return; // Skip REF_TO/REFERENCE_TO keyword-prefixed references (the type name position is offset) if (tr.isReference) return; - // Synthesised inline-array names (`__INLINE_ARRAY_INT`, `__VLA_…`) have no source text to cover. - if (tr.name.startsWith("__")) return; + // Synthesised array names have no source text to cover. Match the two prefixes + // exactly: `__XWORD` is a real elementary type and must keep its token. + if (SYNTHETIC_TYPE_PREFIXES.some((prefix) => tr.name.startsWith(prefix))) return; const upperName = tr.name.toUpperCase(); diff --git a/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts b/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts index 6913dd31..87a8ca73 100644 --- a/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts +++ b/vscode-extension/tests/unit/semantic-tokens-overlap.test.ts @@ -95,6 +95,24 @@ describe("semantic tokens never overlap", () => { expect(tokensOn(CASES["array declaration"], 3)).toEqual([{ text: "Arr", type: "variable" }]); }); + it("keeps the token on __XWORD, which is a real elementary type", () => { + const source = `PROGRAM Main\nVAR\n addr : __XWORD;\nEND_VAR\n ;\nEND_PROGRAM\n`; + expect(tokensOn(source, 3)).toEqual([ + { text: "addr", type: "variable" }, + { text: "__XWORD", type: "type" }, + ]); + }); + + it("keeps the token on a user type whose name starts with underscores", () => { + const source = + `TYPE\n __Count : STRUCT\n A : INT;\n END_STRUCT;\nEND_TYPE\n\n` + + `PROGRAM Main\nVAR\n c : __Count;\nEND_VAR\n ;\nEND_PROGRAM\n`; + expect(tokensOn(source, 9)).toEqual([ + { text: "c", type: "variable" }, + { text: "__Count", type: "type" }, + ]); + }); + it("emits a subscript index once", () => { const onBody = tokensOn(CASES["array subscript"], 6); expect(onBody.filter((t) => t.text === "I")).toHaveLength(1); From 83106e43f5ee0eb9a715ac3abb4a4c951b8de48a Mon Sep 17 00:00:00 2001 From: marcone tenorio Date: Fri, 25 Sep 2026 11:22:04 +0200 Subject: [PATCH 3/7] refactor(runtime): compile the header-only runtime as C++14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime is included by strucpp's emitted code, which on an Arduino target is compiled by the board core's own toolchain at whatever standard that core picked. Half the catalogue sits below C++17 — the five mbed cores hard-code `-std=gnu++14`, and avr, megaavr, samd and two vendor cores are on `gnu++11` — so every C++17 construct reachable from a generated file was a C++ block that could not build there. Several headers already carried a "KEEP THIS HEADER C++14-CLEAN" banner for exactly that reason; `iec_std_lib.hpp`, the largest one, never did and drifted. What changed, all of it inside the runtime: - `std::trait_v` -> `std::trait::value`, 31 sites across three headers. This alone took the full runtime surface from 147 errors to 0 under `-std=c++14`; the rest were cascades. - Inline variables: the 14 constants become plain `constexpr` (internal linkage per TU, the pre-C++17 idiom), and the three mutable slots — `__CURRENT_TIME_NS` in both its threaded and single-threaded forms, and `__CURRENT_DT_NS` — become a function-local static reached through a reference, so no use site changes. - `if constexpr`: `ABS`, `iec_convert_value` and `MOD` move to tag dispatch; `MAX` and `MIN` recurse onto the two-argument overload that already existed in the file instead of testing `sizeof...(rest)`. `iec_test.hpp` includes `iec_std_lib.hpp` rather than forward-declaring `__CURRENT_TIME_NS`: the slot is no longer a plain object a declaration can restate, and the header is the honest dependency. The CLI now drives g++ at `-std=c++14` for both `test` and `repl`, so the suite is the regression guard — a C++17 construct reintroduced into the runtime fails there rather than in a user's firmware build. Verified: `-std=c++14 -pedantic` clean (0 errors, 0 warnings) and `-std=c++17` unchanged at 0; the full suite green at 2435 passed / 7 skipped across 97 files, including the integration tests that compile and run the generated C++. Not addressed: `arduino:sam` (Arduino Due) stays out. Its gcc 4.8.3 tops out at `gnu++1y` and implements neither relaxed constexpr nor the C++14 rule that a constexpr member function is not implicitly const, and it rejects the generic lambda the structure-initializer codegen emits. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X3cRP8dMZScXDKhDXvimmM --- CLAUDE.md | 4 +- README.md | 10 +- docs/ARCHITECTURE.md | 2 +- docs/CLI.md | 4 +- docs/REPL.md | 2 +- docs/RUNTIME.md | 2 +- src/backend/struct-init-codegen.ts | 4 +- src/node/cli.ts | 4 +- src/runtime/README.md | 2 +- src/runtime/include/debug_dispatch.hpp | 4 +- src/runtime/include/iec_date.hpp | 4 +- src/runtime/include/iec_dt.hpp | 2 +- src/runtime/include/iec_ptr.hpp | 20 ++-- src/runtime/include/iec_std_lib.hpp | 137 +++++++++++++++++-------- src/runtime/include/iec_string.hpp | 2 +- src/runtime/include/iec_subrange.hpp | 2 +- src/runtime/include/iec_time.hpp | 12 +-- src/runtime/include/iec_tod.hpp | 2 +- src/runtime/include/iec_traits.hpp | 2 +- src/runtime/include/iec_types.hpp | 14 +-- src/runtime/include/iec_var.hpp | 2 +- src/runtime/test/iec_test.hpp | 7 +- 22 files changed, 147 insertions(+), 97 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 434e8a07..5ceed6f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 diff --git a/README.md b/README.md index 7b7482b2..50fa45e6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # 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). @@ -13,13 +13,13 @@ STruC++ compiles PLC programs written in [Structured Text](https://en.wikipedia. ## 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 with any C++14 compiler (g++, clang++, MSVC). 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). --- @@ -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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0442c37f..a997dda2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/CLI.md b/docs/CLI.md index e9c1b1dc..579a7975 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -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. @@ -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 diff --git a/docs/REPL.md b/docs/REPL.md index de4e9947..9710013e 100644 --- a/docs/REPL.md +++ b/docs/REPL.md @@ -151,7 +151,7 @@ Programs without an explicit interval default to 20ms. The `--build` flag requires: -- `g++` with C++17 support (customizable with `--gpp `) +- `g++` with C++14 support (customizable with `--gpp `) - A C compiler (`cc` or `gcc`, customizable with `--cc `) Both are used during the build step only -- the resulting binary is standalone. diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index f2feedd1..56335530 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -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`) diff --git a/src/backend/struct-init-codegen.ts b/src/backend/struct-init-codegen.ts index 061bb2b2..e9a938f6 100644 --- a/src/backend/struct-init-codegen.ts +++ b/src/backend/struct-init-codegen.ts @@ -3,7 +3,7 @@ /** * 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); * @@ -11,7 +11,7 @@ * * 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. * diff --git a/src/node/cli.ts b/src/node/cli.ts index d6b7e77a..7d6a7c19 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -747,7 +747,7 @@ function runTestMode(options: CLIOptions): void { execFileSync( options.gpp, [ - "-std=c++17", + "-std=c++14", `-I${runtimeIncludeDir}`, `-I${testRuntimeDir}`, `-I${tempDir}`, @@ -1168,7 +1168,7 @@ async function main(): Promise { // 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}`, diff --git a/src/runtime/README.md b/src/runtime/README.md index 3c19bda5..2f91af01 100644 --- a/src/runtime/README.md +++ b/src/runtime/README.md @@ -7,7 +7,7 @@ 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 (GCC 5+, Clang 3.4+, MSVC 2015+) - Google Test (automatically downloaded by CMake) ### Build Instructions diff --git a/src/runtime/include/debug_dispatch.hpp b/src/runtime/include/debug_dispatch.hpp index 144f391c..684a53fe 100644 --- a/src/runtime/include/debug_dispatch.hpp +++ b/src/runtime/include/debug_dispatch.hpp @@ -338,7 +338,7 @@ struct PtrOps { // 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. // --------------------------------------------------------------------------- -inline constexpr TypeOps type_ops[TAG__COUNT] = { +constexpr TypeOps type_ops[TAG__COUNT] = { /*BOOL */ { &force_impl, &unforce_impl, &read_impl, &write_impl, sizeof(BOOL_t) }, /*SINT */ { &force_impl, &unforce_impl, &read_impl, &write_impl, sizeof(SINT_t) }, /*USINT */ { &force_impl, &unforce_impl, &read_impl, &write_impl, sizeof(USINT_t) }, @@ -382,7 +382,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 }, /*SINT */ { &ptr_impl }, /*USINT */ { &ptr_impl }, diff --git a/src/runtime/include/iec_date.hpp b/src/runtime/include/iec_date.hpp index 7ff70660..ba768d27 100644 --- a/src/runtime/include/iec_date.hpp +++ b/src/runtime/include/iec_date.hpp @@ -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; diff --git a/src/runtime/include/iec_dt.hpp b/src/runtime/include/iec_dt.hpp index ac9ef1da..192b3a10 100644 --- a/src/runtime/include/iec_dt.hpp +++ b/src/runtime/include/iec_dt.hpp @@ -46,7 +46,7 @@ namespace strucpp { // 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; +constexpr int64_t DT_NS_PER_DAY = IEC_NS_PER_DAY; // --------------------------------------------------------------------------- // Construction helpers diff --git a/src/runtime/include/iec_ptr.hpp b/src/runtime/include/iec_ptr.hpp index 8a2945fb..fb8eaf75 100644 --- a/src/runtime/include/iec_ptr.hpp +++ b/src/runtime/include/iec_ptr.hpp @@ -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, int> = 0> + template::value, int> = 0> IEC_Ptr operator+(N n) const noexcept { return IEC_Ptr(static_cast(ptr_) + static_cast(n)); } - template, int> = 0> + template::value, int> = 0> IEC_Ptr operator-(N n) const noexcept { return IEC_Ptr(static_cast(ptr_) - static_cast(n)); } @@ -149,13 +149,13 @@ class IEC_Ptr { return IEC_Ptr(static_cast(ptr_) - static_cast(n.get())); } - template, int> = 0> + template::value, int> = 0> IEC_Ptr& operator+=(N n) noexcept { ptr_ = static_cast(ptr_) + static_cast(n); return *this; } - template, int> = 0> + template::value, int> = 0> IEC_Ptr& operator-=(N n) noexcept { ptr_ = static_cast(ptr_) - static_cast(n); return *this; @@ -222,27 +222,27 @@ class IEC_Ptr { bool operator!=(std::nullptr_t) const noexcept { return ptr_ != nullptr; } // Comparison with IECVar (for CODESYS: PT < DWORD_END) - template, int> = 0> + template::value, int> = 0> bool operator<(IECVar other) const noexcept { return reinterpret_cast(ptr_) < static_cast(other.get()); } - template, int> = 0> + template::value, int> = 0> bool operator>(IECVar other) const noexcept { return reinterpret_cast(ptr_) > static_cast(other.get()); } - template, int> = 0> + template::value, int> = 0> bool operator<=(IECVar other) const noexcept { return reinterpret_cast(ptr_) <= static_cast(other.get()); } - template, int> = 0> + template::value, int> = 0> bool operator>=(IECVar other) const noexcept { return reinterpret_cast(ptr_) >= static_cast(other.get()); } - template, int> = 0> + template::value, int> = 0> bool operator==(IECVar other) const noexcept { return reinterpret_cast(ptr_) == static_cast(other.get()); } - template, int> = 0> + template::value, int> = 0> bool operator!=(IECVar other) const noexcept { return reinterpret_cast(ptr_) != static_cast(other.get()); } diff --git a/src/runtime/include/iec_std_lib.hpp b/src/runtime/include/iec_std_lib.hpp index ecab805c..4d5b2cfe 100644 --- a/src/runtime/include/iec_std_lib.hpp +++ b/src/runtime/include/iec_std_lib.hpp @@ -193,16 +193,33 @@ struct ConfigurationInstance { * ABS - Absolute value * Input: ANY_NUM, Output: ANY_NUM (same type) */ +namespace detail { +// Tag dispatch instead of `if constexpr`: the runtime is C++14 (see the header +// banner). Each overload is the branch that C++17 would have discarded. +template +inline T iec_abs_impl(T value, V v, std::true_type /*floating*/, std::false_type) noexcept { + (void)value; + return T(std::abs(v)); +} +template +inline T iec_abs_impl(T value, V v, std::false_type, std::true_type /*signed*/) noexcept { + (void)value; + return T(v < 0 ? -v : v); +} +template +inline T iec_abs_impl(T value, V, std::false_type, std::false_type /*unsigned*/) noexcept { + return value; +} +} // namespace detail + template = 0> inline T ABS(T value) noexcept { auto v = iec_unwrap(value); - if constexpr (std::is_floating_point_v) { - return T(std::abs(v)); - } else if constexpr (std::is_signed_v) { - return T(v < 0 ? -v : v); - } else { - return value; - } + using V = decltype(v); + return detail::iec_abs_impl( + value, v, + std::integral_constant::value>{}, + std::integral_constant::value && std::is_signed::value>{}); } /** @@ -252,7 +269,7 @@ inline T EXPT(T base, T exponent) noexcept { // Mixed-type EXPT: allows e.g. EXPT(INT, REAL) → returns LREAL template, std::decay_t>>> + typename = std::enable_if_t, std::decay_t>::value>> inline IEC_LREAL EXPT(T1 base, T2 exponent) noexcept { return IEC_LREAL(std::pow(static_cast(iec_unwrap(base)), static_cast(iec_unwrap(exponent)))); } @@ -395,7 +412,7 @@ inline T LIMIT(T mn, T in, T mx) noexcept { // Mixed-type MIN/MAX/LIMIT/SEL overloads (OSCAT mixes e.g. INT with DINT) template, std::decay_t>, int> = 0> + std::enable_if_t, std::decay_t>::value, int> = 0> inline auto MAX(T a, U b) noexcept { using CT = std::common_type_t; auto va = static_cast(iec_unwrap(a)); @@ -404,7 +421,7 @@ inline auto MAX(T a, U b) noexcept { } template, std::decay_t>, int> = 0> + std::enable_if_t, std::decay_t>::value, int> = 0> inline auto MIN(T a, U b) noexcept { using CT = std::common_type_t; auto va = static_cast(iec_unwrap(a)); @@ -415,8 +432,8 @@ inline auto MIN(T a, U b) noexcept { template inline auto LIMIT(T1 mn, T2 in, T3 mx) noexcept -> std::enable_if_t< - !(std::is_same_v, std::decay_t> && - std::is_same_v, std::decay_t>), + !(std::is_same, std::decay_t>::value && + std::is_same, std::decay_t>::value), std::common_type_t> { using CT = std::common_type_t; auto vmn = static_cast(iec_unwrap(mn)); @@ -428,7 +445,7 @@ inline auto LIMIT(T1 mn, T2 in, T3 mx) noexcept } template, std::decay_t>, int> = 0> + std::enable_if_t, std::decay_t>::value, int> = 0> inline auto SEL(IEC_BOOL g, T in0, U in1) noexcept { using CT = std::common_type_t; return iec_unwrap(g) ? static_cast(iec_unwrap(in1)) : static_cast(iec_unwrap(in0)); @@ -623,7 +640,7 @@ inline T SHL(T in, IEC_INT n) noexcept { // Mixed-type shift count overloads (OSCAT uses various integer types for shift amount) template = 0, - std::enable_if_t, IEC_INT>, int> = 0> + std::enable_if_t, IEC_INT>::value, int> = 0> inline T SHL(T in, N n) noexcept { auto shift = static_cast(iec_unwrap(n)); if (shift <= 0) return shift == 0 ? in : T(0); @@ -644,7 +661,7 @@ inline T SHR(T in, IEC_INT n) noexcept { // Mixed-type shift count overloads template = 0, - std::enable_if_t, IEC_INT>, int> = 0> + std::enable_if_t, IEC_INT>::value, int> = 0> inline T SHR(T in, N n) noexcept { auto shift = static_cast(iec_unwrap(n)); if (shift <= 0) return shift == 0 ? in : T(0); @@ -688,7 +705,7 @@ inline T ROL(T in, IEC_INT n) noexcept { // Mixed-type rotate overloads template = 0, - std::enable_if_t, IEC_INT>, int> = 0> + std::enable_if_t, IEC_INT>::value, int> = 0> inline T ROL(T in, N n) noexcept { constexpr int bits = sizeof(iec_underlying_type_t) * 8; auto v = iec_unwrap(in); @@ -715,7 +732,7 @@ inline T ROR(T in, IEC_INT n) noexcept { // Mixed-type rotate overloads template = 0, - std::enable_if_t, IEC_INT>, int> = 0> + std::enable_if_t, IEC_INT>::value, int> = 0> inline T ROR(T in, N n) noexcept { constexpr int bits = sizeof(iec_underlying_type_t) * 8; auto v = iec_unwrap(in); @@ -732,14 +749,24 @@ inline T ROR(T in, N n) noexcept { /** * Helper: round-then-cast for REAL→integer conversions per IEC 61131-3 */ +namespace detail { +template +inline ToVal iec_convert_value_impl(FromVal value, std::true_type /*round*/) noexcept { + return static_cast(std::round(static_cast(value))); +} +template +inline ToVal iec_convert_value_impl(FromVal value, std::false_type) noexcept { + return static_cast(value); +} +} // namespace detail + template 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 && std::is_integral_v) { - return static_cast(std::round(static_cast(value))); - } else { - return static_cast(value); - } + return detail::iec_convert_value_impl( + value, + std::integral_constant::value && + std::is_integral::value>{}); } /** @@ -747,7 +774,7 @@ inline ToVal iec_convert_value(FromVal value) noexcept { */ template inline auto CONVERT(From value) noexcept - -> std::enable_if_t, To> { + -> std::enable_if_t::value, To> { return To(iec_convert_value(iec_unwrap(value))); } @@ -756,7 +783,7 @@ inline auto CONVERT(From value) noexcept */ template inline auto CONVERT(From value) noexcept - -> std::enable_if_t, To> { + -> std::enable_if_t::value, To> { return To(iec_convert_value(value)); } @@ -1232,13 +1259,23 @@ inline T DIV(T a, T b) noexcept { * Input: ANY_NUM, Output: ANY_NUM (same type) * Returns remainder of division */ +namespace detail { +template +inline T iec_mod_impl(T a, T b, std::true_type /*floating*/) noexcept { + return T(std::fmod(static_cast(iec_unwrap(a)), static_cast(iec_unwrap(b)))); +} +template +inline T iec_mod_impl(T a, T b, std::false_type) noexcept { + return T(iec_unwrap(a) % iec_unwrap(b)); +} +} // namespace detail + template = 0> inline T MOD(T a, T b) noexcept { - if constexpr (std::is_floating_point_v>) { - return T(std::fmod(static_cast(iec_unwrap(a)), static_cast(iec_unwrap(b)))); - } else { - return T(iec_unwrap(a) % iec_unwrap(b)); - } + return detail::iec_mod_impl( + a, b, + std::integral_constant>::value>{}); } // ============================================================================= @@ -1323,14 +1360,12 @@ inline T XOR(T first, T second, Args... rest) noexcept { * Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type) * Returns the maximum of two or more values */ +// Three-or-more arguments only; the two-argument base case is defined above. +// Recursion instead of `if constexpr (sizeof...(rest) > 0)`, which is C++17. template = 0> -inline T MAX(T first, T second, Args... rest) noexcept { +inline T MAX(T first, T second, T third, 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; - } + return MAX(current_max, third, rest...); } /** @@ -1338,14 +1373,12 @@ inline T MAX(T first, T second, Args... rest) noexcept { * Input: ANY_ELEMENTARY, Output: ANY_ELEMENTARY (same type) * Returns the minimum of two or more values */ +// Three-or-more arguments only; the two-argument base case is defined above. +// Recursion instead of `if constexpr (sizeof...(rest) > 0)`, which is C++17. template = 0> -inline T MIN(T first, T second, Args... rest) noexcept { +inline T MIN(T first, T second, T third, 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; - } + return MIN(current_min, third, rest...); } /** @@ -1382,9 +1415,21 @@ inline T MOVE(T value) noexcept { // 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; +inline int64_t& __current_time_ns_slot() { + static thread_local int64_t slot = 0; + return slot; +} +static thread_local int64_t& __CURRENT_TIME_NS = __current_time_ns_slot(); #else -inline int64_t __CURRENT_TIME_NS = 0; +// One slot shared by every translation unit, reached through a reference so +// every existing use site still reads and writes the plain name. A header-scope +// `inline` variable would say this in one line, but that is C++17 and the +// runtime targets C++14. +inline int64_t& __current_time_ns_slot() { + static int64_t slot = 0; + return slot; +} +static int64_t& __CURRENT_TIME_NS = __current_time_ns_slot(); #endif /** @@ -1406,7 +1451,11 @@ inline IEC_TIME TIME() { * 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; +inline int64_t& __current_dt_ns_slot() { + static int64_t slot = 0; + return slot; +} +static int64_t& __CURRENT_DT_NS = __current_dt_ns_slot(); /** * CURRENT_DT() — wall-clock date-and-time. diff --git a/src/runtime/include/iec_string.hpp b/src/runtime/include/iec_string.hpp index d47414d6..de62c395 100644 --- a/src/runtime/include/iec_string.hpp +++ b/src/runtime/include/iec_string.hpp @@ -18,7 +18,7 @@ // // In this header (and anything it includes) do NOT use C++17/20 features // unguarded. In particular: -// * `std::trait_v` -> `std::trait::value` +// * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs diff --git a/src/runtime/include/iec_subrange.hpp b/src/runtime/include/iec_subrange.hpp index 360414a0..863a0471 100644 --- a/src/runtime/include/iec_subrange.hpp +++ b/src/runtime/include/iec_subrange.hpp @@ -18,7 +18,7 @@ // core's `-std` — gnu++14 on every mbed-based core (Nano RP2040 Connect, Nano // 33 BLE, Opta, GIGA, Portenta, Edge). To stay safe if/when this header enters // that chain, keep it C++14-clean. Do NOT use C++17/20 features unguarded: -// * `std::trait_v` -> `std::trait::value` +// * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs diff --git a/src/runtime/include/iec_time.hpp b/src/runtime/include/iec_time.hpp index fc3bfeee..3a024a32 100644 --- a/src/runtime/include/iec_time.hpp +++ b/src/runtime/include/iec_time.hpp @@ -37,12 +37,12 @@ namespace strucpp { // import them for arithmetic that mixes literal scalars with TIME values // (e.g. `MUL_TIME(t, 60)` for a 1-minute multiplier without recomputing the // constant). -inline constexpr int64_t NS_PER_US = 1000LL; -inline constexpr int64_t NS_PER_MS = 1000000LL; -inline constexpr int64_t NS_PER_S = 1000000000LL; -inline constexpr int64_t NS_PER_M = 60LL * NS_PER_S; -inline constexpr int64_t NS_PER_H = 60LL * NS_PER_M; -inline constexpr int64_t NS_PER_D = 24LL * NS_PER_H; +constexpr int64_t NS_PER_US = 1000LL; +constexpr int64_t NS_PER_MS = 1000000LL; +constexpr int64_t NS_PER_S = 1000000000LL; +constexpr int64_t NS_PER_M = 60LL * NS_PER_S; +constexpr int64_t NS_PER_H = 60LL * NS_PER_M; +constexpr int64_t NS_PER_D = 24LL * NS_PER_H; // --------------------------------------------------------------------------- // Conversion: TIME → integer count of the requested unit (truncated) diff --git a/src/runtime/include/iec_tod.hpp b/src/runtime/include/iec_tod.hpp index 905f50cc..4cae977b 100644 --- a/src/runtime/include/iec_tod.hpp +++ b/src/runtime/include/iec_tod.hpp @@ -43,7 +43,7 @@ namespace strucpp { // self-contained. Duplicate inline-constexpr declarations at namespace // scope are legal as long as the value matches; clients including both // headers see one definition. -inline constexpr int64_t TOD_NS_PER_DAY = IEC_NS_PER_DAY; +constexpr int64_t TOD_NS_PER_DAY = IEC_NS_PER_DAY; // Normalise a signed nanosecond count into the canonical [0, 24h) TOD // range. Negative inputs roll over from "before midnight today" to diff --git a/src/runtime/include/iec_traits.hpp b/src/runtime/include/iec_traits.hpp index 4897f315..553df70f 100644 --- a/src/runtime/include/iec_traits.hpp +++ b/src/runtime/include/iec_traits.hpp @@ -18,7 +18,7 @@ // // In this header (and anything it includes) do NOT use C++17/20 features // unguarded. In particular: -// * `std::trait_v` -> `std::trait::value` +// * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs diff --git a/src/runtime/include/iec_types.hpp b/src/runtime/include/iec_types.hpp index 7b9f8985..5a8215c1 100644 --- a/src/runtime/include/iec_types.hpp +++ b/src/runtime/include/iec_types.hpp @@ -18,7 +18,7 @@ // // In this header (and anything it includes) do NOT use C++17/20 features // unguarded. In particular: -// * `std::trait_v` -> `std::trait::value` +// * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs @@ -132,8 +132,8 @@ using LREAL_t = double; * the conversion helpers now convert BETWEEN those types, a divergence would * produce wrong values rather than merely duplicated text. */ -inline constexpr int64_t IEC_SECONDS_PER_DAY = 24LL * 60LL * 60LL; -inline constexpr int64_t IEC_NS_PER_DAY = IEC_SECONDS_PER_DAY * 1000000000LL; +constexpr int64_t IEC_SECONDS_PER_DAY = 24LL * 60LL * 60LL; +constexpr int64_t IEC_NS_PER_DAY = IEC_SECONDS_PER_DAY * 1000000000LL; /** IEC TIME - Duration in nanoseconds */ using TIME_t = int64_t; @@ -258,15 +258,15 @@ template<> struct IECTypeCategory { using type = AnyStringTag; }; /** Concept for ANY_BIT types */ template -concept IECAnyBit = std::is_same_v::type, AnyBitTag>; +concept IECAnyBit = std::is_same::type, AnyBitTag>::value; /** Concept for ANY_INT types */ template -concept IECAnyInt = std::is_same_v::type, AnyIntTag>; +concept IECAnyInt = std::is_same::type, AnyIntTag>::value; /** Concept for ANY_REAL types */ template -concept IECAnyReal = std::is_same_v::type, AnyRealTag>; +concept IECAnyReal = std::is_same::type, AnyRealTag>::value; /** Concept for ANY_NUM types */ template @@ -274,7 +274,7 @@ concept IECAnyNum = IECAnyInt || IECAnyReal; /** Concept for ANY_DATE types */ template -concept IECAnyDate = std::is_same_v::type, AnyDateTag>; +concept IECAnyDate = std::is_same::type, AnyDateTag>::value; #endif // C++20 diff --git a/src/runtime/include/iec_var.hpp b/src/runtime/include/iec_var.hpp index ade5a66c..43d17421 100644 --- a/src/runtime/include/iec_var.hpp +++ b/src/runtime/include/iec_var.hpp @@ -18,7 +18,7 @@ // // In this header (and anything it includes) do NOT use C++17/20 features // unguarded. In particular: -// * `std::trait_v` -> `std::trait::value` +// * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs diff --git a/src/runtime/test/iec_test.hpp b/src/runtime/test/iec_test.hpp index 87016f61..1108f4ad 100644 --- a/src/runtime/test/iec_test.hpp +++ b/src/runtime/test/iec_test.hpp @@ -27,10 +27,11 @@ #include #include -namespace strucpp { +// The scan-cycle time slot lives in iec_std_lib.hpp. Included rather than +// forward-declared: it is no longer a plain object a declaration can restate. +#include "iec_std_lib.hpp" -// Forward declaration of scan-cycle time global (defined in iec_std_lib.hpp) -extern int64_t __CURRENT_TIME_NS; +namespace strucpp { // ============================================================================ // Value formatting From 13cc118ecd448b6b16f5d432a9eb229db95a2ffb Mon Sep 17 00:00:00 2001 From: marcone tenorio Date: Fri, 25 Sep 2026 16:28:50 +0200 Subject: [PATCH 4/7] fix(runtime): include at file scope, not inside the namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `iec_traits.hpp` and `iec_types.hpp` declare their C++20 concepts inside `namespace strucpp`, and the `#include ` that those declarations need sat there with them, behind `#if __cplusplus >= 202002L`. Including a standard header from inside a namespace nests everything it declares — and everything it pulls in transitively — under that namespace. The guard never opened before, so the bug never fired: generated code was compiled at a standard the editor pinned, and it pinned C++17. Letting the board's core choose the standard is enough to open it, and an ESP32 compiles the sketch at `gnu++2b`. What comes out is the whole of `std` reachable only as `strucpp::std`, with the failure surfacing deep in libstdc++: error: 'is_constant_evaluated' is not a member of 'std'; did you mean 'strucpp::std::is_constant_evaluated'? The include moves to file scope in both headers, keeping the version guard. The runtime now compiles clean at C++14, 17, 20 and 23 — a range that was never exercised, because nothing built it above 17. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X3cRP8dMZScXDKhDXvimmM --- src/runtime/include/iec_traits.hpp | 10 ++++++++-- src/runtime/include/iec_types.hpp | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/runtime/include/iec_traits.hpp b/src/runtime/include/iec_traits.hpp index 553df70f..78dd13cc 100644 --- a/src/runtime/include/iec_traits.hpp +++ b/src/runtime/include/iec_traits.hpp @@ -44,6 +44,14 @@ #include #include "iec_types.hpp" +// must be included at file scope: the concept declarations that use +// it live inside `namespace strucpp`, and including a standard header from +// there would nest the whole of `std` under it. Guarded because the runtime +// targets C++14 and only some cores compile the sketch at C++20 or above. +#if __cplusplus >= 202002L +#include +#endif + namespace strucpp { // Forward declarations @@ -533,8 +541,6 @@ using enable_if_iec_pointer = std::enable_if_t, int>; #if __cplusplus >= 202002L -#include - /** Concept for IEC types */ template concept IECType = is_iec_type_v; diff --git a/src/runtime/include/iec_types.hpp b/src/runtime/include/iec_types.hpp index 5a8215c1..11fb709e 100644 --- a/src/runtime/include/iec_types.hpp +++ b/src/runtime/include/iec_types.hpp @@ -42,6 +42,14 @@ #include #include +// must be included at file scope: the concept declarations that use +// it live inside `namespace strucpp`, and including a standard header from +// there would nest the whole of `std` under it. Guarded because the runtime +// targets C++14 and only some cores compile the sketch at C++20 or above. +#if __cplusplus >= 202002L +#include +#endif + namespace strucpp { // ============================================================================= @@ -254,8 +262,6 @@ template<> struct IECTypeCategory { using type = AnyStringTag; }; #if __cplusplus >= 202002L -#include - /** Concept for ANY_BIT types */ template concept IECAnyBit = std::is_same::type, AnyBitTag>::value; From dc37d458f2123493a1a00ed51c0ef96a631f77f7 Mon Sep 17 00:00:00 2001 From: marcone tenorio Date: Mon, 28 Sep 2026 20:13:04 +0200 Subject: [PATCH 5/7] =?UTF-8?q?fix(runtime):=20finish=20the=20C++14=20port?= =?UTF-8?q?=20=E2=80=94=20test=20header,=20MAX/MIN=20surface,=20test=20sta?= =?UTF-8?q?ndard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things the port left behind, all of them only reachable once something actually compiled at C++14. `iec_test.hpp` still used `std::is_same_v` in `assert_eq` / `assert_neq`, so `strucpp --test` failed to compile the moment the CLI moved to `-std=c++14`. Those were the last two `_v<>` spellings in the runtime. The variadic `MAX` / `MIN` had been narrowed: dropping `if constexpr` in favour of recursion introduced a named `T third`, which forced the third argument to the same type as the first two. `MAX(a, b, 0)` and `MAX(a, b, someDint)` compiled before and stopped compiling, at both c++14 and c++17, because the recursion could no longer reach the mixed-type two-argument overload that exists for OSCAT. Generated ST is unaffected — codegen casts every argument — but hand-written C++ calling into the runtime is. `third` now carries its own type again. The integration suite hard-coded `-std=c++17` at 17 call sites, which is why neither showed up: the suite stayed green by never compiling at the standard the product ships. It now goes through a single `CXX_STD` constant. Codegen still emits `inline` variables for VAR_GLOBAL, which are C++17 in C++14 code. GCC takes them as an extension and warns; strict C++14 and MSVC reject them. They stay, and the reason is now written at both emission sites: the debug table takes each variable's address into PROGMEM, which needs a constant expression, so a reference or a static-local does not work — on AVR a project declaring a global then fails with "variable 'debug_arr_0' with dynamic initialization put into program memory area". Replacing them needs `extern` in the header plus a definition in one designated translation unit, and codegen currently spreads implementation across several. `integer-literal-exact-cpp` carries `-Wno-c++17-extensions` for the same reason, since it opts into `-pedantic-errors` for an unrelated check. Also narrows the README toolchain claim to what is exercised (g++ 7.3+), and corrects comments describing `inline constexpr` constants that are plain `constexpr` now, and therefore one copy per translation unit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X3cRP8dMZScXDKhDXvimmM --- README.md | 2 +- src/backend/codegen.ts | 3 +++ src/runtime/README.md | 3 ++- src/runtime/include/debug_dispatch.hpp | 5 +++++ src/runtime/include/iec_dt.hpp | 4 ++-- src/runtime/include/iec_std_lib.hpp | 18 ++++++++++-------- src/runtime/include/iec_tod.hpp | 5 ++--- src/runtime/test/iec_test.hpp | 4 ++-- tests/integration/cpp-compile.test.ts | 5 +++-- tests/integration/debug-table-cpp.test.ts | 12 ++++++------ .../dope614-string-force-length.test.ts | 4 ++-- .../global-local-operand-mix-cpp.test.ts | 3 ++- .../integer-literal-exact-cpp.test.ts | 10 ++++++++-- tests/integration/oscat-gpp-compile.test.ts | 3 ++- tests/integration/repl-runner.test.ts | 3 ++- .../structure-initialization-cpp.test.ts | 2 +- tests/integration/test-helpers.ts | 17 +++++++++++++---- 17 files changed, 66 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 50fa45e6..bb305d4f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ STruC++ compiles PLC programs written in [Structured Text](https://en.wikipedia. **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 and the runtime compile with any C++14 compiler (g++, clang++, MSVC). 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). +**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). --- diff --git a/src/backend/codegen.ts b/src/backend/codegen.ts index e2f61ab6..41043c9a 100644 --- a/src/backend/codegen.ts +++ b/src/backend/codegen.ts @@ -1259,6 +1259,8 @@ export class CodeGenerator { 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( @@ -2742,6 +2744,7 @@ export class CodeGenerator { ); emittedAny = true; } + // Must stay a real object — generated_debug.cpp takes `&name` into PROGMEM. this.emitHeader( `inline GlobalVar<${cppType}> ${gvar.name}{${initVal}};`, ); diff --git a/src/runtime/README.md b/src/runtime/README.md index 2f91af01..4f82eae6 100644 --- a/src/runtime/README.md +++ b/src/runtime/README.md @@ -7,7 +7,8 @@ This directory contains the C++ runtime library for STruC++, providing IEC 61131 ### Prerequisites - CMake 3.14 or higher -- C++14 compatible compiler (GCC 5+, Clang 3.4+, MSVC 2015+) +- 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 diff --git a/src/runtime/include/debug_dispatch.hpp b/src/runtime/include/debug_dispatch.hpp index 684a53fe..bae4a820 100644 --- a/src/runtime/include/debug_dispatch.hpp +++ b/src/runtime/include/debug_dispatch.hpp @@ -337,6 +337,11 @@ 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. // --------------------------------------------------------------------------- constexpr TypeOps type_ops[TAG__COUNT] = { /*BOOL */ { &force_impl, &unforce_impl, &read_impl, &write_impl, sizeof(BOOL_t) }, diff --git a/src/runtime/include/iec_dt.hpp b/src/runtime/include/iec_dt.hpp index 192b3a10..78c051d3 100644 --- a/src/runtime/include/iec_dt.hpp +++ b/src/runtime/include/iec_dt.hpp @@ -44,8 +44,8 @@ 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. +// 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; // --------------------------------------------------------------------------- diff --git a/src/runtime/include/iec_std_lib.hpp b/src/runtime/include/iec_std_lib.hpp index 4d5b2cfe..1c1ced5a 100644 --- a/src/runtime/include/iec_std_lib.hpp +++ b/src/runtime/include/iec_std_lib.hpp @@ -1362,10 +1362,11 @@ inline T XOR(T first, T second, Args... rest) noexcept { */ // Three-or-more arguments only; the two-argument base case is defined above. // Recursion instead of `if constexpr (sizeof...(rest) > 0)`, which is C++17. -template = 0> -inline T MAX(T first, T second, T third, Args... rest) noexcept { - T current_max = iec_unwrap(first) > iec_unwrap(second) ? first : second; - return MAX(current_max, third, rest...); +// `third` keeps its own type so the recursion still reaches the mixed-type +// two-argument overload — OSCAT passes INT and DINT to the same call. +template = 0> +inline auto MAX(T first, T second, U third, Args... rest) noexcept { + return MAX(MAX(first, second), third, rest...); } /** @@ -1375,10 +1376,11 @@ inline T MAX(T first, T second, T third, Args... rest) noexcept { */ // Three-or-more arguments only; the two-argument base case is defined above. // Recursion instead of `if constexpr (sizeof...(rest) > 0)`, which is C++17. -template = 0> -inline T MIN(T first, T second, T third, Args... rest) noexcept { - T current_min = iec_unwrap(first) < iec_unwrap(second) ? first : second; - return MIN(current_min, third, rest...); +// `third` keeps its own type so the recursion still reaches the mixed-type +// two-argument overload — OSCAT passes INT and DINT to the same call. +template = 0> +inline auto MIN(T first, T second, U third, Args... rest) noexcept { + return MIN(MIN(first, second), third, rest...); } /** diff --git a/src/runtime/include/iec_tod.hpp b/src/runtime/include/iec_tod.hpp index 4cae977b..5dadabf7 100644 --- a/src/runtime/include/iec_tod.hpp +++ b/src/runtime/include/iec_tod.hpp @@ -40,9 +40,8 @@ namespace strucpp { // Constants // --------------------------------------------------------------------------- // Mirror of iec_time.hpp's NS_PER_X but scoped here too so iec_tod.hpp is -// self-contained. Duplicate inline-constexpr declarations at namespace -// scope are legal as long as the value matches; clients including both -// headers see one definition. +// self-contained. Plain `constexpr` under C++14, so each header carries +// its own internal-linkage copy and the values must agree. constexpr int64_t TOD_NS_PER_DAY = IEC_NS_PER_DAY; // Normalise a signed nanosecond count into the canonical [0, 24h) TOD diff --git a/src/runtime/test/iec_test.hpp b/src/runtime/test/iec_test.hpp index 1108f4ad..82a310c0 100644 --- a/src/runtime/test/iec_test.hpp +++ b/src/runtime/test/iec_test.hpp @@ -189,7 +189,7 @@ struct TestContext { * ASSERT_EQ: check actual == expected (mixed types, e.g. IECStringVar vs const char*) */ template, std::decay_t>, int> = 0> + std::enable_if_t, std::decay_t>::value, int> = 0> bool assert_eq(T actual, U expected, const char* actual_expr, const char* expected_expr, int line, const char* msg = "") { @@ -219,7 +219,7 @@ struct TestContext { * ASSERT_NEQ: check actual != expected (mixed types) */ template, std::decay_t>, int> = 0> + std::enable_if_t, std::decay_t>::value, int> = 0> bool assert_neq(T actual, U expected, const char* actual_expr, const char* expected_expr, int line, const char* msg = "") { diff --git a/tests/integration/cpp-compile.test.ts b/tests/integration/cpp-compile.test.ts index a4dc8f6a..43c8ed7d 100644 --- a/tests/integration/cpp-compile.test.ts +++ b/tests/integration/cpp-compile.test.ts @@ -17,6 +17,7 @@ import { createPCH, compileWithGpp as compileWithGppHelper, compileAndRunStandalone as compileAndRunHelper, + CXX_STD, } from './test-helpers.js'; const describeIfGpp = hasGpp ? describe : describe.skip; @@ -142,7 +143,7 @@ describeIfGpp('C++ Compilation Tests', () => { let ok = true; let diag = ''; try { - execSync(`g++ -std=c++17 ${flag} -I"${runtimeInclude}" "${cpp}" -o "${out}"`, { + execSync(`g++ -std=${CXX_STD} ${flag} -I"${runtimeInclude}" "${cpp}" -o "${out}"`, { stdio: 'pipe', }); } catch (e) { @@ -201,7 +202,7 @@ describeIfGpp('C++ Compilation Tests', () => { let ok = true; let diag = ''; try { - execSync(`g++ -std=c++17 ${flag} -I"${runtimeInclude}" "${cpp}" -o "${out}"`, { stdio: 'pipe' }); + execSync(`g++ -std=${CXX_STD} ${flag} -I"${runtimeInclude}" "${cpp}" -o "${out}"`, { stdio: 'pipe' }); execSync(`"${out}"`, { stdio: 'pipe' }); // exit 0 ⇒ shared COUNTER == 2 } catch (e) { ok = false; diff --git a/tests/integration/debug-table-cpp.test.ts b/tests/integration/debug-table-cpp.test.ts index de5a8fc5..2ac19cea 100644 --- a/tests/integration/debug-table-cpp.test.ts +++ b/tests/integration/debug-table-cpp.test.ts @@ -23,7 +23,7 @@ import * as os from "os"; import { execSync } from "child_process"; import { compile } from "../../src/index.js"; import { discoverStlibs } from "../../src/node/library-loader.js"; -import { hasGpp } from "./test-helpers.js"; +import { hasGpp, CXX_STD } from "./test-helpers.js"; /** * Harness for the retained-library-FB round trip. Kept out of the test body so * the C++ is readable as C++ rather than as an escaped template literal. @@ -134,7 +134,7 @@ describeIfGpp("generated debug table compiles", () => { for (const target of [programPath, tablePath]) { try { execSync( - `g++ -std=c++17 -fsyntax-only -I"${RUNTIME_INCLUDE}" -I"${dir}" "${target}" 2>&1`, + `g++ -std=${CXX_STD} -fsyntax-only -I"${RUNTIME_INCLUDE}" -I"${dir}" "${target}" 2>&1`, { encoding: "utf-8" }, ); } catch (e) { @@ -460,7 +460,7 @@ int main() { const bin = path.join(dir, "gate"); execSync( - `g++ -std=c++17 -I"${RUNTIME_INCLUDE}" -I"${dir}" ` + + `g++ -std=${CXX_STD} -I"${RUNTIME_INCLUDE}" -I"${dir}" ` + `-o "${bin}" "${path.join(dir, "main.cpp")}" ` + `"${path.join(dir, "generated.cpp")}" "${path.join(dir, "generated_debug.cpp")}"`, { encoding: "utf-8" }, @@ -604,7 +604,7 @@ int main() { const bin2 = path.join(dir, "ptr"); execSync( - `g++ -std=c++17 -I"${RUNTIME_INCLUDE}" -I"${dir}" ` + + `g++ -std=${CXX_STD} -I"${RUNTIME_INCLUDE}" -I"${dir}" ` + `-o "${bin2}" "${path.join(dir, "main.cpp")}" ` + `"${path.join(dir, "generated.cpp")}" "${path.join(dir, "generated_debug.cpp")}"`, { encoding: "utf-8" }, @@ -740,7 +740,7 @@ int main() { const bin = path.join(dir, "retain"); execSync( - `g++ -std=c++17 -I"${RUNTIME_INCLUDE}" -I"${dir}" -o "${bin}" ` + + `g++ -std=${CXX_STD} -I"${RUNTIME_INCLUDE}" -I"${dir}" -o "${bin}" ` + `"${path.join(dir, "main.cpp")}" "${path.join(dir, "generated.cpp")}" ` + `"${path.join(dir, "generated_debug.cpp")}"`, { encoding: "utf-8" }, @@ -795,7 +795,7 @@ END_CONFIGURATION`, const bin = path.join(dir, "libfb-retain"); execSync( - `g++ -std=c++17 -I"${RUNTIME_INCLUDE}" -I"${dir}" -o "${bin}" ` + + `g++ -std=${CXX_STD} -I"${RUNTIME_INCLUDE}" -I"${dir}" -o "${bin}" ` + `"${path.join(dir, "main.cpp")}" ` + `"${path.join(dir, "generated_debug.cpp")}" ${sources.join(" ")}`, { encoding: "utf-8" }, diff --git a/tests/integration/dope614-string-force-length.test.ts b/tests/integration/dope614-string-force-length.test.ts index ae607c25..e7ca716a 100644 --- a/tests/integration/dope614-string-force-length.test.ts +++ b/tests/integration/dope614-string-force-length.test.ts @@ -26,7 +26,7 @@ import * as path from "path"; import * as os from "os"; import { execSync } from "child_process"; import { compile } from "../../src/index.js"; -import { hasGpp, RUNTIME_INCLUDE_PATH, cxxEnv } from "./test-helpers.js"; +import { hasGpp, RUNTIME_INCLUDE_PATH, cxxEnv, CXX_STD } from "./test-helpers.js"; const describeIfGpp = hasGpp ? describe : describe.skip; @@ -93,7 +93,7 @@ ${driverBody} const bin = path.join(dir, name); execSync( - `g++ -std=c++17 -I"${RUNTIME_INCLUDE_PATH}" -I"${dir}" ` + + `g++ -std=${CXX_STD} -I"${RUNTIME_INCLUDE_PATH}" -I"${dir}" ` + `-o "${bin}" "${path.join(dir, "main.cpp")}" ` + `"${path.join(dir, "generated.cpp")}" "${path.join(dir, "generated_debug.cpp")}"`, { encoding: "utf-8", env: cxxEnv }, diff --git a/tests/integration/global-local-operand-mix-cpp.test.ts b/tests/integration/global-local-operand-mix-cpp.test.ts index cb4deb0e..6f72d575 100644 --- a/tests/integration/global-local-operand-mix-cpp.test.ts +++ b/tests/integration/global-local-operand-mix-cpp.test.ts @@ -33,6 +33,7 @@ import { compileAndRunStandalone, RUNTIME_INCLUDE_PATH, cxxEnv, + CXX_STD, } from "./test-helpers.js"; const describeIfGpp = hasGpp ? describe : describe.skip; @@ -544,7 +545,7 @@ describeIfGpp( const binPath = path.join(tempDir, testName); fs.writeFileSync(srcPath, source); execSync( - `g++ -std=c++17 -O2 -DSTRUCPP_THREADED -pthread -I"${RUNTIME_INCLUDE_PATH}" "${srcPath}" -o "${binPath}" 2>&1`, + `g++ -std=${CXX_STD} -O2 -DSTRUCPP_THREADED -pthread -I"${RUNTIME_INCLUDE_PATH}" "${srcPath}" -o "${binPath}" 2>&1`, { encoding: "utf-8", env: cxxEnv }, ); return execSync(`"${binPath}"`, { diff --git a/tests/integration/integer-literal-exact-cpp.test.ts b/tests/integration/integer-literal-exact-cpp.test.ts index ab5d29d7..b3370cad 100644 --- a/tests/integration/integer-literal-exact-cpp.test.ts +++ b/tests/integration/integer-literal-exact-cpp.test.ts @@ -1,6 +1,6 @@ /** * End-to-end proof that 64-bit integer literals survive the whole pipeline: the - * generated C++ must compile with g++ -std=c++17 AND print back the digits the + * generated C++ must compile with g++ -std=c++14 AND print back the digits the * ST source wrote. * * Compiling is not sufficient evidence here. `9007199254740993` lowered to @@ -57,7 +57,13 @@ describeIfGpp("64-bit integer literals — generated C++", () => { testName, // An unsuffixed `18446744073709551615` is a GCC *extension* that warns // rather than fails, so the warning has to be the failure here. - extraFlags: ["-pedantic-errors", "-Werror=overflow"], + // + // `-Wno-c++17-extensions`: codegen emits `inline` variables for + // VAR_GLOBAL, which at c++14 is such an extension, and `-pedantic-errors` + // aborts on it before reaching the literal this test is about. They cannot + // become a static-local — generated_debug.cpp takes their address into + // PROGMEM, which needs a constant. + extraFlags: ["-pedantic-errors", "-Wno-c++17-extensions", "-Werror=overflow"], mainCode: `#include \n\nint main() {\n using namespace strucpp;\n${mainBody}\n return 0;\n}\n`, }); } diff --git a/tests/integration/oscat-gpp-compile.test.ts b/tests/integration/oscat-gpp-compile.test.ts index df650a81..694e1189 100644 --- a/tests/integration/oscat-gpp-compile.test.ts +++ b/tests/integration/oscat-gpp-compile.test.ts @@ -26,6 +26,7 @@ import { RUNTIME_INCLUDE_PATH, TEST_RUNTIME_PATH, cxxEnv, + CXX_STD, } from "./test-helpers.js"; import { execSync } from "child_process"; @@ -182,7 +183,7 @@ describe.skipIf(!hasGpp || !oscatStlibAvailable)( const binaryPath = path.join(tempDir, "oscat_test"); const gppCmd = [ "g++", - "-std=c++17", + `-std=${CXX_STD}`, `-include "${pchPath}"`, `-I"${RUNTIME_INCLUDE_PATH}"`, `-I"${TEST_RUNTIME_PATH}"`, diff --git a/tests/integration/repl-runner.test.ts b/tests/integration/repl-runner.test.ts index aaa89477..91c15654 100644 --- a/tests/integration/repl-runner.test.ts +++ b/tests/integration/repl-runner.test.ts @@ -21,6 +21,7 @@ import { RUNTIME_INCLUDE_PATH, REPL_PATH, cxxEnv, + CXX_STD, } from './test-helpers.js'; const describeIfCompilers = hasGpp && hasCc ? describe : describe.skip; @@ -72,7 +73,7 @@ describeIfCompilers('REPL Runner Integration Tests', () => { // Compile C++ and link with pre-compiled isocline.o + PCH execSync( - `g++ -std=c++17 -include "${pchPath}" -I"${RUNTIME_INCLUDE_PATH}" -I"${REPL_PATH}" -I"${tempDir}" "${mainPath}" "${cppPath}" "${isoclineObj}" -o "${binPath}" 2>&1`, + `g++ -std=${CXX_STD} -include "${pchPath}" -I"${RUNTIME_INCLUDE_PATH}" -I"${REPL_PATH}" -I"${tempDir}" "${mainPath}" "${cppPath}" "${isoclineObj}" -o "${binPath}" 2>&1`, { encoding: 'utf-8', env: cxxEnv }, ); diff --git a/tests/integration/structure-initialization-cpp.test.ts b/tests/integration/structure-initialization-cpp.test.ts index 81069d20..840bad29 100644 --- a/tests/integration/structure-initialization-cpp.test.ts +++ b/tests/integration/structure-initialization-cpp.test.ts @@ -1,6 +1,6 @@ /** * End-to-end tests for IEC 61131-3 `structure_initialization` and composite - * declaration initialisers: the generated C++ must compile with g++ -std=c++17 + * declaration initialisers: the generated C++ must compile with g++ -std=c++14 * AND hold the values the ST source asked for. * * Compiling is not enough on its own here. The lowering has to get three things diff --git a/tests/integration/test-helpers.ts b/tests/integration/test-helpers.ts index 16e803e2..d18f362b 100644 --- a/tests/integration/test-helpers.ts +++ b/tests/integration/test-helpers.ts @@ -18,6 +18,15 @@ import { analyzeTestFile } from '../../src/semantic/analyzer.js'; /** Resolved path to the C++ runtime headers */ export const RUNTIME_INCLUDE_PATH = path.resolve(__dirname, '../../src/runtime/include'); +/** + * The C++ standard every integration test compiles at. + * + * Pinned here rather than spelled out per call site: the runtime is C++14 and + * `cli.ts` builds at c++14, so a site left at c++17 makes the suite green on + * code the shipped compiler would reject. + */ +export const CXX_STD = 'c++14'; + /** Resolved path to the REPL support files (isocline, etc.) */ export const REPL_PATH = path.resolve(__dirname, '../../src/runtime/repl'); @@ -90,7 +99,7 @@ export function createPCH(tempDir: string): string { fs.writeFileSync(pchHppPath, PCH_INCLUDES); execSync( - `g++ -std=c++17 -x c++-header -I"${RUNTIME_INCLUDE_PATH}" "${pchHppPath}" -o "${pchGchPath}" 2>&1`, + `g++ -std=${CXX_STD} -x c++-header -I"${RUNTIME_INCLUDE_PATH}" "${pchHppPath}" -o "${pchGchPath}" 2>&1`, { encoding: 'utf-8', env: cxxEnv }, ); @@ -164,14 +173,14 @@ export function compileWithGpp(opts: CompileWithGppOptions): CompileResult { try { if (syntaxOnly) { execSync( - `g++ -std=c++17 -fsyntax-only ${includeFlags} ${flagsStr} "${cppPath}" 2>&1`, + `g++ -std=${CXX_STD} -fsyntax-only ${includeFlags} ${flagsStr} "${cppPath}" 2>&1`, { encoding: 'utf-8', env: cxxEnv }, ); return { success: true }; } else { const binPath = path.join(tempDir, testName); execSync( - `g++ -std=c++17 ${includeFlags} ${flagsStr} "${cppPath}" ${objectsStr} -o "${binPath}" 2>&1`, + `g++ -std=${CXX_STD} ${includeFlags} ${flagsStr} "${cppPath}" ${objectsStr} -o "${binPath}" 2>&1`, { encoding: 'utf-8', env: cxxEnv }, ); return { success: true, outputPath: binPath }; @@ -313,7 +322,7 @@ export function runE2ETestPipeline( const gppCommand = [ 'g++', - '-std=c++17', + `-std=${CXX_STD}`, `-I${RUNTIME_INCLUDE_PATH}`, `-I${TEST_RUNTIME_PATH}`, `-I${tempDir}`, From 03b9f6241237de5730dfff728a6bdfa2ca7cb430 Mon Sep 17 00:00:00 2001 From: marcone tenorio Date: Mon, 28 Sep 2026 21:16:42 +0200 Subject: [PATCH 6/7] docs(runtime): finish correcting the type_ops comment The linkage note was added but the two sentences it contradicts were left in place: the table is no longer `inline`, so "kept inline so no separate .cpp is required" and "whatever `inline constexpr` suggests" both described a spelling that is gone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X3cRP8dMZScXDKhDXvimmM --- src/runtime/include/debug_dispatch.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/include/debug_dispatch.hpp b/src/runtime/include/debug_dispatch.hpp index bae4a820..7c3aff2f 100644 --- a/src/runtime/include/debug_dispatch.hpp +++ b/src/runtime/include/debug_dispatch.hpp @@ -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. From ad1431ed76f178325b3a7595624c0eea6d40fd5b Mon Sep 17 00:00:00 2001 From: marcone tenorio Date: Mon, 28 Sep 2026 22:26:19 +0200 Subject: [PATCH 7/7] chore(release): bump version to 0.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A minor rather than a patch: the header-only runtime now compiles as C++14 instead of C++17, which changes what a consumer's toolchain has to offer. Produced with `npm version 0.7.0 --no-git-tag-version` plus a build, so both `package-lock.json` entries move together and `src/version-build.ts` is regenerated by `rebuild-libs.mjs` rather than edited — the version string the npm tarball ships comes from that file, and a hand edit is what leaves a re-bundling consumer reporting 0.0.0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X3cRP8dMZScXDKhDXvimmM --- package-lock.json | 4 ++-- package.json | 2 +- src/version-build.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93a84826..8d32d6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "strucpp", - "version": "0.6.9", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "strucpp", - "version": "0.6.9", + "version": "0.7.0", "license": "GPL-3.0-or-later", "dependencies": { "chevrotain": "^11.0.0" diff --git a/package.json b/package.json index 6bc58df9..7fcffec8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/version-build.ts b/src/version-build.ts index d9d887ef..f35c1d1a 100644 --- a/src/version-build.ts +++ b/src/version-build.ts @@ -2,4 +2,4 @@ // Copyright (C) 2025 Autonomy / OpenPLC Project // AUTO-GENERATED by scripts/rebuild-libs.mjs from package.json. Do not edit by hand — // any changes are overwritten on the next build. -export const STRUCPP_VERSION_BUILD = "0.6.9"; +export const STRUCPP_VERSION_BUILD = "0.7.0";