From f97cd281d46a2710505ccd7e652f270a0497b0ab Mon Sep 17 00:00:00 2001 From: Manuele Conti Date: Mon, 13 Jul 2026 22:08:08 +0200 Subject: [PATCH] Fix C++ POU local state variables --- .../editor/compiler/compiler-module.ts | 23 ++++++++++-- .../PLC/__tests__/preprocess-pous.test.ts | 17 +++++++++ .../shared/utils/PLC/preprocess-pous.ts | 26 +++++++++----- .../cpp/__tests__/generateCBlocksCode.test.ts | 35 +++++++++++++++---- .../__tests__/generateCBlocksHeader.test.ts | 31 ++++++++++++---- .../shared/utils/cpp/generateCBlocksCode.ts | 21 +++-------- .../shared/utils/cpp/generateCBlocksHeader.ts | 10 ++---- .../cpp/__tests__/generateSTCode.test.ts | 32 +++++++++++++---- src/frontend/utils/cpp/cppPouVariables.ts | 22 ++++++++++++ src/frontend/utils/cpp/generateSTCode.ts | 7 ++-- 10 files changed, 166 insertions(+), 58 deletions(-) create mode 100644 src/frontend/utils/cpp/cppPouVariables.ts diff --git a/src/backend/editor/compiler/compiler-module.ts b/src/backend/editor/compiler/compiler-module.ts index c8b914fc9..91ef42ac6 100644 --- a/src/backend/editor/compiler/compiler-module.ts +++ b/src/backend/editor/compiler/compiler-module.ts @@ -79,6 +79,23 @@ type ProjectDataWithCppPous = PLCProjectData & { originalCppPous?: CppPouDataCode[] } +/** + * Keep generated C++ metadata aligned with the POU interfaces used to build + * the ST bridge. The sidecar carries the original C++ source, while the + * preprocessed POU is the authoritative source for its current variables. + */ +const getCppPousForGeneration = (projectData: ProjectDataWithCppPous): CppPouDataCode[] => { + return (projectData.originalCppPous ?? []).map((cppPou) => { + const processedPou = projectData.pous.find((pou) => pou.data.name === cppPou.name) as + | { data?: { variables?: PLCVariable[] } } + | undefined + return { + ...cppPou, + variables: processedPou?.data?.variables ?? cppPou.variables, + } + }) +} + /** * Post-build PLC start retry loop bounds. Why these numbers: * - 5000 ms total: longer than the slowest STOP transition observed @@ -118,7 +135,7 @@ import { app as electronApp, dialog, MessageChannelMain } from 'electron' import type { MessagePortMain } from 'electron/main' import JSZip from 'jszip' -import type { PlatformOption } from '../../../middleware/shared/ports/types' +import type { PlatformOption, PLCVariable } from '../../../middleware/shared/ports/types' import { BoardInfoResolver } from '../../shared/hardware/board-info-resolver' import type { PackageManifest } from '../package-manager' import { PackageManagerModule } from '../package-manager' @@ -1259,7 +1276,7 @@ class CompilerModule { sourceTargetFolderPath: string, handleOutputData: HandleOutputDataCallback, ) { - const originalCppPous = projectData.originalCppPous || [] + const originalCppPous = getCppPousForGeneration(projectData) if (originalCppPous.length === 0) { handleOutputData('No C/C++ blocks found, skipping c_blocks.h generation', 'info') @@ -1292,7 +1309,7 @@ class CompilerModule { _boardRuntime: string, handleOutputData: HandleOutputDataCallback, ) { - const originalCppPous = projectData.originalCppPous || [] + const originalCppPous = getCppPousForGeneration(projectData) if (originalCppPous.length === 0) { handleOutputData('No C/C++ blocks found, skipping c_blocks_code.cpp generation', 'info') diff --git a/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts b/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts index 109380992..ba01a57d9 100644 --- a/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts +++ b/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts @@ -245,6 +245,23 @@ describe('preprocessPous — C++', () => { expect(vars.some((v) => v.name === 'hasBeenInitialized')).toBe(true) }) + it('keeps C++ sidecar variables aligned with the generated ST bridge', () => { + const variables = [ + makeVariable('Enable', 'input', 'BOOL'), + makeVariable('PrevSeq', 'local', 'USINT'), + makeVariable('NewData', 'output', 'BOOL'), + ] + const project = makeProjectData([makeCppPou('can_rx', validCppCode, variables)]) + const logger = collectLog() + const { projectData } = preprocessPous(project, false, logger.log) + + const body = projectData.pous[0].body.value as string + expect(body).toContain('vars.PREVSEQ = &PREVSEQ;') + expect(projectData.originalCppPous?.[0].variables.map((v) => v.name)).toEqual( + expect.arrayContaining(['PrevSeq', 'hasBeenInitialized']), + ) + }) + it('skips C++ processing when no C++ POUs exist', () => { const project = makeProjectData([makeStPou('Main', 'x := 1;')]) const logger = collectLog() diff --git a/src/backend/shared/utils/PLC/preprocess-pous.ts b/src/backend/shared/utils/PLC/preprocess-pous.ts index bced661cf..74a381759 100644 --- a/src/backend/shared/utils/PLC/preprocess-pous.ts +++ b/src/backend/shared/utils/PLC/preprocess-pous.ts @@ -136,17 +136,25 @@ function preprocessPous(projectData: PLCProjectData, isSimulator: boolean, log: return { projectData: processedProjectData as ProjectDataWithCpp, validationFailed: true } } - processedProjectData = addCppLocalVariables(processedProjectData) - - const originalCppPousData = cppPous.map((pou) => ({ - name: pou.name, - code: + const originalCppCodeByName = new Map( + cppPous.map((pou) => [ + pou.name, /* istanbul ignore next -- defensive: cppPous filter guarantees language === 'cpp' */ pou.body.language === 'cpp' ? (pou.body as { language: string; value: string }).value : '', - variables: - /* istanbul ignore next -- defensive: interface may be undefined */ - pou.interface?.variables ?? [], - })) + ]), + ) + + processedProjectData = addCppLocalVariables(processedProjectData) + + const originalCppPousData = processedProjectData.pous + .filter((pou: PLCPou) => pou.body.language === 'cpp') + .map((pou) => ({ + name: pou.name, + code: originalCppCodeByName.get(pou.name) ?? '', + variables: + /* istanbul ignore next -- defensive: interface may be undefined */ + pou.interface?.variables ?? [], + })) processedProjectData.pous = processedProjectData.pous.map((pou: PLCPou) => { if (pou.body.language === 'cpp') { diff --git a/src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts b/src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts index ba3382fdb..5b18c6500 100644 --- a/src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts +++ b/src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts @@ -1,7 +1,7 @@ import type { PLCVariable } from '../../../../../middleware/shared/ports/types' import { generateCBlocksCode } from '../generateCBlocksCode' -const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({ +const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({ name, class: cls, type: { definition: 'base-type', value: baseType }, @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): debug: false, }) -const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({ +const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({ name, class: cls, type: { @@ -169,13 +169,13 @@ describe('generateCBlocksCode', () => { expect(result).toContain('// comment about setup') }) - it('filters variables by class (only input and output)', () => { + it('generates struct fields and macros for local state variables', () => { const variables: PLCVariable[] = [ makeScalarVar('inVar', 'input', 'INT'), { - name: 'localVar', + name: 'PrevSeq', class: 'local', - type: { definition: 'base-type', value: 'INT' }, + type: { definition: 'base-type', value: 'USINT' }, location: '', documentation: '', debug: false, @@ -188,9 +188,30 @@ describe('generateCBlocksCode', () => { expect(result).toContain('#define inVar') expect(result).toContain('#define outVar') - expect(result).not.toContain('#define localVar') + expect(result).toContain('strucpp::IEC_USINT *PREVSEQ;') + expect(result).toContain('#define PrevSeq (*(vars->PREVSEQ))') expect(result).toContain('#undef inVar') expect(result).toContain('#undef outVar') - expect(result).not.toContain('#undef localVar') + expect(result).toContain('#undef PrevSeq') + }) + + it('does not expose the generated setup guard as a user C++ macro', () => { + const variables: PLCVariable[] = [ + makeScalarVar('x', 'input', 'INT'), + { + name: 'hasBeenInitialized', + class: 'local', + type: { definition: 'base-type', value: 'BOOL' }, + location: '', + documentation: '', + debug: false, + }, + ] + const code = 'void setup() { }\nvoid loop() { }' + + const result = generateCBlocksCode([{ name: 'test', code, variables }]) + + expect(result).not.toContain('HASBEENINITIALIZED') + expect(result).not.toContain('#define hasBeenInitialized') }) }) diff --git a/src/backend/shared/utils/cpp/__tests__/generateCBlocksHeader.test.ts b/src/backend/shared/utils/cpp/__tests__/generateCBlocksHeader.test.ts index 3f556ee5f..89c8ce776 100644 --- a/src/backend/shared/utils/cpp/__tests__/generateCBlocksHeader.test.ts +++ b/src/backend/shared/utils/cpp/__tests__/generateCBlocksHeader.test.ts @@ -1,7 +1,7 @@ import type { PLCVariable } from '../../../../../middleware/shared/ports/types' import { generateCBlocksHeader } from '../generateCBlocksHeader' -const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({ +const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({ name, class: cls, type: { definition: 'base-type', value: baseType }, @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): debug: false, }) -const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({ +const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({ name, class: cls, type: { @@ -59,13 +59,13 @@ describe('generateCBlocksHeader', () => { expect(result).toContain('extern "C" void myblock_loop(MYBLOCK_VARS *vars);') }) - it('includes only input and output variables in the struct', () => { + it('includes local variables in the C++ POU instance struct', () => { const variables: PLCVariable[] = [ makeScalarVar('inVar', 'input', 'INT'), { - name: 'localVar', + name: 'PrevSeq', class: 'local', - type: { definition: 'base-type', value: 'BOOL' }, + type: { definition: 'base-type', value: 'USINT' }, location: '', documentation: '', debug: false, @@ -76,8 +76,27 @@ describe('generateCBlocksHeader', () => { const result = generateCBlocksHeader([{ name: 'test', variables }]) expect(result).toContain('strucpp::IEC_INT *INVAR;') + expect(result).toContain('strucpp::IEC_USINT *PREVSEQ;') expect(result).toContain('strucpp::IEC_BOOL *OUTVAR;') - expect(result).not.toContain('LOCALVAR') + }) + + it('does not include the generated setup guard in the C++ POU instance struct', () => { + const variables: PLCVariable[] = [ + makeScalarVar('inVar', 'input', 'INT'), + { + name: 'hasBeenInitialized', + class: 'local', + type: { definition: 'base-type', value: 'BOOL' }, + location: '', + documentation: '', + debug: false, + }, + ] + + const result = generateCBlocksHeader([{ name: 'test', variables }]) + + expect(result).toContain('strucpp::IEC_INT *INVAR;') + expect(result).not.toContain('HASBEENINITIALIZED') }) it('generates declarations for multiple pous', () => { diff --git a/src/backend/shared/utils/cpp/generateCBlocksCode.ts b/src/backend/shared/utils/cpp/generateCBlocksCode.ts index e0794ab84..d0dd9074b 100644 --- a/src/backend/shared/utils/cpp/generateCBlocksCode.ts +++ b/src/backend/shared/utils/cpp/generateCBlocksCode.ts @@ -1,3 +1,4 @@ +import { getCppPouStateVariables } from '../../../../frontend/utils/cpp/cppPouVariables' import { generateStructMember, isArrayVariable } from '../../../../frontend/utils/PLC/array-codegen-helpers' import type { PLCVariable } from '../../../../middleware/shared/ports/types' @@ -112,17 +113,12 @@ const processUserCode = (pou: CppPouData): string => { const setupFunctionName = `${pou.name.toLowerCase()}_setup` const loopFunctionName = `${pou.name.toLowerCase()}_loop` - const inputVariables = pou.variables.filter((v) => v.class === 'input') - const outputVariables = pou.variables.filter((v) => v.class === 'output') + const stateVariables = getCppPouStateVariables(pou.variables) let processedCode = `//definition of external blocks - ${pou.name.toUpperCase()}\n` processedCode += `typedef struct {\n` - inputVariables.forEach((variable) => { - processedCode += generateStructMember(variable) - }) - - outputVariables.forEach((variable) => { + stateVariables.forEach((variable) => { processedCode += generateStructMember(variable) }) @@ -131,11 +127,7 @@ const processUserCode = (pou: CppPouData): string => { processedCode += `extern "C" void ${setupFunctionName}(${structName} *vars);\n` processedCode += `extern "C" void ${loopFunctionName}(${structName} *vars);\n\n` - inputVariables.forEach((variable) => { - processedCode += generateDefine(variable) - }) - - outputVariables.forEach((variable) => { + stateVariables.forEach((variable) => { processedCode += generateDefine(variable) }) @@ -153,10 +145,7 @@ const processUserCode = (pou: CppPouData): string => { processedCode += modifiedUserCode processedCode += '\n' - inputVariables.forEach((variable) => { - processedCode += generateUndef(variable) - }) - outputVariables.forEach((variable) => { + stateVariables.forEach((variable) => { processedCode += generateUndef(variable) }) processedCode += '\n' diff --git a/src/backend/shared/utils/cpp/generateCBlocksHeader.ts b/src/backend/shared/utils/cpp/generateCBlocksHeader.ts index f5ec2ed50..f91fa3a9c 100644 --- a/src/backend/shared/utils/cpp/generateCBlocksHeader.ts +++ b/src/backend/shared/utils/cpp/generateCBlocksHeader.ts @@ -1,3 +1,4 @@ +import { getCppPouStateVariables } from '../../../../frontend/utils/cpp/cppPouVariables' import { generateStructMember } from '../../../../frontend/utils/PLC/array-codegen-helpers' import type { PLCVariable } from '../../../../middleware/shared/ports/types' @@ -25,17 +26,12 @@ const generateCBlocksHeader = (cppPous: CppPouData[]): string => { const setupFunctionName = `${pou.name.toLowerCase()}_setup` const loopFunctionName = `${pou.name.toLowerCase()}_loop` - const inputVariables = pou.variables.filter((v) => v.class === 'input') - const outputVariables = pou.variables.filter((v) => v.class === 'output') + const stateVariables = getCppPouStateVariables(pou.variables) headerContent += `//definition of external blocks - ${pou.name.toUpperCase()}\n` headerContent += `typedef struct {\n` - inputVariables.forEach((variable) => { - headerContent += generateStructMember(variable) - }) - - outputVariables.forEach((variable) => { + stateVariables.forEach((variable) => { headerContent += generateStructMember(variable) }) diff --git a/src/frontend/utils/cpp/__tests__/generateSTCode.test.ts b/src/frontend/utils/cpp/__tests__/generateSTCode.test.ts index c0d7d3c68..d7d244a68 100644 --- a/src/frontend/utils/cpp/__tests__/generateSTCode.test.ts +++ b/src/frontend/utils/cpp/__tests__/generateSTCode.test.ts @@ -1,7 +1,7 @@ import type { PLCVariable } from '../../../../middleware/shared/ports/types' import { generateSTCode } from '../generateSTCode' -const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({ +const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({ name, class: cls, type: { definition: 'base-type', value: baseType }, @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): debug: false, }) -const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({ +const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({ name, class: cls, type: { @@ -153,11 +153,11 @@ describe('generateSTCode (cpp)', () => { expect(result).toContain('vars.D = &D[0] - 0;') }) - it('filters out local variables', () => { + it('passes C++ POU local variables as per-instance state', () => { const localVar: PLCVariable = { - name: 'localVal', + name: 'PrevSeq', class: 'local', - type: { definition: 'base-type', value: 'INT' }, + type: { definition: 'base-type', value: 'USINT' }, location: '', documentation: '', debug: false, @@ -168,6 +168,26 @@ describe('generateSTCode (cpp)', () => { allVariables: [makeScalarVar('x', 'input', 'INT'), localVar], }) - expect(result).not.toContain('LOCALVAL') + expect(result).toContain('vars.PREVSEQ = &PREVSEQ;') + }) + + it('does not pass the runtime setup guard to the C++ POU struct', () => { + const result = generateSTCode({ + pouName: 'test', + allVariables: [ + makeScalarVar('x', 'input', 'INT'), + { + name: 'hasBeenInitialized', + class: 'local', + type: { definition: 'base-type', value: 'BOOL' }, + location: '', + documentation: '', + debug: false, + }, + ], + }) + + expect(result).toContain('if hasBeenInitialized = False then') + expect(result).not.toContain('vars.HASBEENINITIALIZED') }) }) diff --git a/src/frontend/utils/cpp/cppPouVariables.ts b/src/frontend/utils/cpp/cppPouVariables.ts new file mode 100644 index 000000000..727d975c4 --- /dev/null +++ b/src/frontend/utils/cpp/cppPouVariables.ts @@ -0,0 +1,22 @@ +import type { PLCVariable } from '../../../middleware/shared/ports/types' + +const CPP_RUNTIME_LOCAL_VARIABLE_NAMES = new Set(['hasBeenInitialized']) + +const isCppRuntimeLocalVariable = (variable: PLCVariable): boolean => { + return CPP_RUNTIME_LOCAL_VARIABLE_NAMES.has(variable.name) +} + +const isCppPouStateVariable = (variable: PLCVariable): boolean => { + if (isCppRuntimeLocalVariable(variable)) return false + + const variableClass = variable.class ?? 'local' + return ( + variableClass === 'input' || variableClass === 'output' || variableClass === 'inOut' || variableClass === 'local' + ) +} + +const getCppPouStateVariables = (variables: PLCVariable[]): PLCVariable[] => { + return variables.filter(isCppPouStateVariable) +} + +export { getCppPouStateVariables, isCppPouStateVariable, isCppRuntimeLocalVariable } diff --git a/src/frontend/utils/cpp/generateSTCode.ts b/src/frontend/utils/cpp/generateSTCode.ts index 9d5a200ba..24d232c04 100644 --- a/src/frontend/utils/cpp/generateSTCode.ts +++ b/src/frontend/utils/cpp/generateSTCode.ts @@ -1,5 +1,6 @@ import type { PLCVariable } from '../../../middleware/shared/ports/types' import { getArrayStartIndex, isArrayVariable } from '../PLC/array-codegen-helpers' +import { getCppPouStateVariables } from './cppPouVariables' type STCodeGenerationParams = { pouName: string @@ -34,16 +35,14 @@ const generateVariableAssignment = (variable: PLCVariable): string => { const generateSTCode = (params: STCodeGenerationParams): string => { const { pouName, allVariables } = params - const inputVariables = allVariables.filter((v) => v.class === 'input') - const outputVariables = allVariables.filter((v) => v.class === 'output') + const stateVariables = getCppPouStateVariables(allVariables) const structName = `${pouName.toUpperCase()}_VARS` const setupFunctionName = `${pouName.toLowerCase()}_setup` const loopFunctionName = `${pouName.toLowerCase()}_loop` let variableAssignments = '' - for (const variable of inputVariables) variableAssignments += generateVariableAssignment(variable) - for (const variable of outputVariables) variableAssignments += generateVariableAssignment(variable) + for (const variable of stateVariables) variableAssignments += generateVariableAssignment(variable) // Header `{external}` block: declare the user-visible struct, fill // the pointer fields. STruC++ emits this body verbatim into the