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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/backend/editor/compiler/compiler-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@
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,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})
}

Comment on lines +82 to +98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 5 'PLCProjectData|originalCppPous|toIpcProjectData|pou\.data\.variables' src

Repository: Autonomy-Logic/openplc-editor

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- compiler module ---'
sed -n '1,120p' src/backend/editor/compiler/compiler-module.ts
printf '%s\n' '--- shared type declarations ---'
rg -n -C 6 'export (type|interface) (PLCProjectData|PLCPou|PLCVariable)|type (PLCProjectData|PLCPou|PLCVariable)' \
  src/backend/shared/types/PLC src/middleware/shared/ports/types.ts
printf '%s\n' '--- preprocess and metadata flow ---'
rg -n -C 8 'function preprocessPous|const preprocessPous|preprocessPous\(|originalCppPous|getCppPousForGeneration|generateCBlocks(Header|Code)' \
  src/backend src/middleware src/main

Repository: Autonomy-Logic/openplc-editor

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- imports and exact helper context ---'
sed -n '116,145p' src/backend/editor/compiler/compiler-module.ts
sed -n '82,100p' src/backend/editor/compiler/compiler-module.ts

printf '%s\n' '--- POU schema definitions ---'
sed -n '80,175p' src/backend/shared/types/PLC/open-plc.ts
sed -n '210,275p' src/backend/shared/types/PLC/open-plc.ts
sed -n '820,865p' src/backend/shared/types/PLC/open-plc.ts

printf '%s\n' '--- C++ sidecar types and generator contracts ---'
sed -n '1,180p' src/backend/shared/utils/cpp/generateCBlocksCode.ts
sed -n '1,140p' src/backend/shared/utils/cpp/generateCBlocksHeader.ts

printf '%s\n' '--- helper call sites ---'
rg -n -C 4 'getCppPousForGeneration' src/backend/editor/compiler/compiler-module.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 18915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

compiler = Path("src/backend/editor/compiler/compiler-module.ts").read_text()
schema = Path("src/backend/shared/types/PLC/open-plc.ts").read_text()

assert "projectData.pous.find((pou) => pou.data.name === cppPou.name) as" in compiler
assert re.search(
    r"import type \{ PlatformOption, PLCVariable \} from .*ports/types",
    compiler,
)
assert len(re.findall(r"\bPLCVariable\b", compiler)) == 1, "PLCVariable has other uses"

pou_schema = schema[schema.index("const PLCPouSchema"):schema.index("type PLCPou =")]
variants = re.findall(
    r"z\.object\(\{\s*type: z\.literal\('[^']+'\),\s*data: (PLC\w+Schema)",
    pou_schema,
)
assert variants == ["PLCProgramSchema", "PLCFunctionSchema", "PLCFunctionBlockSchema"]

for name in variants:
    match = re.search(
        rf"const {name} = z\.object\(\{{(.*?)\n\}}\)",
        schema,
        re.S,
    )
    assert match and re.search(r"\bvariables:\s*z\.array\(PLCVariableSchema\)", match.group(1)), name

print("The assertion is present.")
print("PLCVariable is imported only for the assertion.")
print("Every PLCPou variant exposes data.variables as PLCVariable[].")
print("The inferred backend POU type is sufficient for the proposed assertion removal.")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\bPLCVariable\b' src/backend/editor/compiler/compiler-module.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 313


Remove the unchecked POU type assertion.

PLCProjectData already exposes pou.data.variables as PLCVariable[] for every POU variant. Remove the assertion and the unused PLCVariable import.

Proposed fix
-import type { PlatformOption, PLCVariable } from '../../../middleware/shared/ports/types'
+import type { PlatformOption } from '../../../middleware/shared/ports/types'

-    const processedPou = projectData.pous.find((pou) => pou.data.name === cppPou.name) as
-      | { data?: { variables?: PLCVariable[] } }
-      | undefined
+    const processedPou = projectData.pous.find((pou) => pou.data.name === cppPou.name)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 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,
}
})
}
/**
* 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)
return {
...cppPou,
variables: processedPou?.data?.variables ?? cppPou.variables,
}
})
}
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/editor/compiler/compiler-module.ts` around lines 82 - 98, Update
getCppPousForGeneration to use the inferred type from projectData.pous.find
directly, removing the unchecked cast around processedPou. Delete the now-unused
PLCVariable import while preserving the existing variables fallback behavior.

Source: Coding guidelines


🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'originalCppPous|preprocessPous|getCppPousForGeneration|handleGenerateCBlocks(Header|Code)|compileForDebugger|toIpcProjectData' src

Repository: Autonomy-Logic/openplc-editor

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- compiler-module symbols ---'
rg -n -C 10 'getCppPousForGeneration|compileProgram|compileForDebugger|handleGenerateCBlocksHeader|handleGenerateCBlocksCode|originalCppPous|projectData\.pous' src/backend/editor/compiler/compiler-module.ts

printf '%s\n' '--- preprocessing and conversion ---'
sed -n '1,210p' src/backend/shared/utils/PLC/preprocess-pous.ts
sed -n '1,90p' src/middleware/adapters/editor/compiler-adapter.ts
sed -n '160,325p' src/middleware/adapters/editor/compiler-adapter.ts

printf '%s\n' '--- compiler module entry-point declarations and call sites ---'
rg -n -C 12 'compileProgram|compileForDebugger|handleGenerateCBlocks(Header|Code)' src/backend/editor/compiler/compiler-module.ts src/main/modules/ipc/main.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- compileProgram data flow ---'
sed -n '2408,2575p' src/backend/editor/compiler/compiler-module.ts
sed -n '2575,2924p' src/backend/editor/compiler/compiler-module.ts

printf '%s\n' '--- compileForDebugger data flow ---'
sed -n '2924,3122p' src/backend/editor/compiler/compiler-module.ts

printf '%s\n' '--- exact adapter conversion and preprocess call sites ---'
sed -n '176,215p' src/middleware/adapters/editor/compiler-adapter.ts
sed -n '286,320p' src/middleware/adapters/editor/compiler-adapter.ts

printf '%s\n' '--- pipeline C++ consumers ---'
rg -n -C 6 'buildCBlocksFromPous|handleGenerateCBlocks(Header|Code)|originalCppPous|runCompilePipeline' src/backend/shared/compile src/backend/editor/compiler/compiler-module.ts

printf '%s\n' '--- type and shape declarations ---'
rg -n -C 5 'interface .*Pou|type .*Pou|PLCProjectData|IpcProjectData|CppPouDataCode|PLCVariable' src/middleware/shared/ports/types.ts src/backend/editor/compiler/compiler-module.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

adapter = Path("src/middleware/adapters/editor/compiler-adapter.ts").read_text()
preprocess = Path("src/backend/shared/utils/PLC/preprocess-pous.ts").read_text()
compiler = Path("src/backend/editor/compiler/compiler-module.ts").read_text()
pipeline = Path("src/backend/shared/compile/pipeline.ts").read_text()

def require(text, needle, label):
    if needle not in text:
        raise AssertionError(f"missing {label}: {needle}")

def ordered(text, needles, label):
    positions = [text.index(needle) for needle in needles]
    if positions != sorted(positions):
        raise AssertionError(f"wrong order in {label}: {positions}")

# Build and debugger both preprocess, then convert the processed result.
require(adapter, "const { projectData: processedData, validationFailed } = preprocessPous(", "build preprocessing")
require(adapter, "const ipcData = toIpcProjectData(processedData)", "build IPC conversion")
require(adapter, "async compileForDebug(", "debug entry point")
debug = adapter[adapter.index("async compileForDebug("):]
require(debug, "const { projectData: processedData, validationFailed } = preprocessPous(", "debug preprocessing")
require(debug, "const ipcData = toIpcProjectData(processedData)", "debug IPC conversion")

# The converter forwards the sidecar when it exists.
require(adapter, "...(data.originalCppPous ? { originalCppPous:", "sidecar forwarding")

# C++ preprocessing records variables after local-variable augmentation and
# replaces the C++ POU body only after the sidecar is assembled.
ordered(
    preprocess,
    [
        "processedProjectData = addCppLocalVariables(processedProjectData)",
        "const originalCppPousData = processedProjectData.pous",
        "processedProjectData.pous = processedProjectData.pous.map",
        "projectDataWithCpp.originalCppPous = originalCppPousData",
    ],
    "C++ preprocessing",
)

# compileProgram passes the received projectData to the shared pipeline.
require(compiler, "const result = await runCompilePipeline(", "compileProgram pipeline call")
compile_call = compiler[compiler.index("const result = await runCompilePipeline("):]
require(compile_call, "projectData,", "compileProgram projectData handoff")

# The debugger uses the sidecar and generated header/code helpers.
debug_backend = compiler[compiler.index("async compileForDebugger("):]
for needle in (
    "originalCppPous?.length",
    "await this.handleGenerateCBlocksHeader(projectData",
    "await this.handleGenerateCBlocksCode(projectData",
):
    require(debug_backend, needle, f"debug backend consumer {needle}")

# The shared pipeline consumes the sidecar for C-block generation.
require(pipeline, "const originalCppPous = processedData.originalCppPous ?? []", "pipeline sidecar read")
require(pipeline, "buildCBlocksFromPous(originalCppPous as never)", "pipeline C-block generation")

print("metadata flow checks passed")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

adapter = Path("src/middleware/adapters/editor/compiler-adapter.ts").read_text()
preprocess = Path("src/backend/shared/utils/PLC/preprocess-pous.ts").read_text()
compiler = Path("src/backend/editor/compiler/compiler-module.ts").read_text()
pipeline = Path("src/backend/shared/compile/pipeline.ts").read_text()

def require(text, needle, label):
    if needle not in text:
        raise AssertionError(f"missing {label}: {needle}")

def ordered(text, needles, label):
    positions = [text.index(needle) for needle in needles]
    if positions != sorted(positions):
        raise AssertionError(f"wrong order in {label}: {positions}")

build = adapter[:adapter.index("async compileForDebug(")]
debug = adapter[adapter.index("async compileForDebug("):]
require(build, "const { projectData: processedData, validationFailed } = preprocessPous(", "build preprocessing")
require(build, "const ipcData = toIpcProjectData(processedData)", "build IPC conversion")
require(debug, "const { projectData: processedData, validationFailed } = preprocessPous(", "debug preprocessing")
require(debug, "const ipcData = toIpcProjectData(processedData)", "debug IPC conversion")
require(adapter, "...(data.originalCppPous ? { originalCppPous:", "sidecar forwarding")

cpp_start = preprocess.index("// --- C++ processing ---")
cpp_end = preprocess.index("// --- SoftMotion:", cpp_start)
cpp = preprocess[cpp_start:cpp_end]
ordered(
    cpp,
    [
        "processedProjectData = addCppLocalVariables(processedProjectData)",
        "const originalCppPousData = processedProjectData.pous",
        "processedProjectData.pous = processedProjectData.pous.map",
        "projectDataWithCpp.originalCppPous = originalCppPousData",
    ],
    "C++ preprocessing",
)

compile_start = compiler.index("const result = await runCompilePipeline(")
compile_call = compiler[compile_start:]
require(compile_call, "projectData,", "compileProgram projectData handoff")

debug_backend = compiler[compiler.index("async compileForDebugger("):]
for needle in (
    "originalCppPous?.length",
    "await this.handleGenerateCBlocksHeader(projectData",
    "await this.handleGenerateCBlocksCode(projectData",
):
    require(debug_backend, needle, f"debug backend consumer {needle}")

require(pipeline, "const originalCppPous = processedData.originalCppPous ?? []", "pipeline sidecar read")
require(pipeline, "buildCBlocksFromPous(originalCppPous as never)", "pipeline C-block generation")

print("metadata flow checks passed")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 195


Replace the type assertion with explicit narrowing. The assertion violates the TypeScript guidelines and bypasses validation of the IPC POU shape. Use a type guard before reading data.variables.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/editor/compiler/compiler-module.ts` around lines 82 - 98, Replace
the type assertion in getCppPousForGeneration with an explicit type guard that
validates the matched POU has a data object before accessing data.variables.
Preserve the existing fallback to cppPou.variables when no valid processed POU
or variables are available.

/**
* Post-build PLC start retry loop bounds. Why these numbers:
* - 5000 ms total: longer than the slowest STOP transition observed
Expand Down Expand Up @@ -118,7 +135,7 @@
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 { formatPackageIntegrityError, PackageManagerModule } from '../package-manager'
import { CreateXMLFile } from '../utils'
Expand Down Expand Up @@ -491,8 +508,8 @@

checkStrucppAvailability(): MethodsResult<string> {
try {
const { getVersion } = loadStrucpp()

Check warning on line 511 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe array destructuring of a tuple element with an error typed value
return { success: true, data: getVersion() }

Check warning on line 512 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe call of a(n) `error` type typed value

Check warning on line 512 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
} catch {
throw new Error('STruC++ not available. Run "npm run setup:binaries" to install it.')
}
Expand Down Expand Up @@ -1242,7 +1259,7 @@
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')
Expand Down Expand Up @@ -1275,7 +1292,7 @@
_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')
Expand Down Expand Up @@ -3037,7 +3054,7 @@
_mainProcessPort.postMessage({
logLevel,
message: data,
...(compileError ? { compileError } : {}),

Check warning on line 3057 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
})
},
{ hasCBlocks, pous: knownPous, libraries, missingLibraries },
Expand Down
17 changes: 17 additions & 0 deletions src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
26 changes: 17 additions & 9 deletions src/backend/shared/utils/PLC/preprocess-pous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,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') {
Expand Down
35 changes: 28 additions & 7 deletions src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand All @@ -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')
})
})
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand All @@ -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', () => {
Expand Down
21 changes: 5 additions & 16 deletions src/backend/shared/utils/cpp/generateCBlocksCode.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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'
Expand Down
10 changes: 3 additions & 7 deletions src/backend/shared/utils/cpp/generateCBlocksHeader.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
})

Expand Down
32 changes: 26 additions & 6 deletions src/frontend/utils/cpp/__tests__/generateSTCode.test.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand All @@ -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')
})
})
Loading
Loading