Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* End-to-end regression for issue #944 ("Type Conversion TO_INT not
* working") at the level `generateGraphicalPou` actually runs at: a full
* `TranspilePou` + `TranspileProject`, exactly like the compile pipeline
* (`backend/shared/compile/pipeline.ts`) feeds it.
*
* Reproduces the reported project shape: a PROGRAM with a REAL variable
* (`O_R`) wired into a `TO_INT` conversion block on an FBD network,
* feeding an INT output variable — the exact pattern that produced
* `_TMP_TO_INT6913197_OUT := TO_INT(O_R);` in the issue's generated
* `webserver_program.st` and made matiec fail with `';' missing at the
* end of statement`.
*/

import type { RFNode } from '../../walker/types'
import type { TranspilePou, TranspileProject } from '../../types'
import { generateGraphicalPou } from '../pou-graphical'

function inputVariableNode(id: string, name: string): RFNode {
return {
id,
type: 'input-variable',
position: { x: 0, y: 0 },
data: { variant: 'input-variable', variable: { name }, executionOrder: 0 },
}
}

function outputVariableNode(id: string, name: string): RFNode {
return {
id,
type: 'output-variable',
position: { x: 100, y: 0 },
data: { variant: 'output-variable', variable: { name }, executionOrder: 0 },
}
}

function conversionBlockNode(id: string, typeName: string, numericId: string): RFNode {
const destinationType = typeName.match(/^TO_([A-Z][A-Z0-9]*)$/)?.[1] ?? 'ANY'
return {
id,
type: 'block',
position: { x: 50, y: 0 },
data: {
variant: {
name: typeName,
type: 'function',
variables: [
{ name: 'IN', class: 'input', type: { definition: 'base-type', value: 'ANY' } },
{ name: 'OUT', class: 'output', type: { definition: 'base-type', value: destinationType } },
],
},
numericId,
executionOrder: 0,
},
}
}

function emptyProject(pous: TranspilePou[]): TranspileProject {
return {
dataTypes: [],
pous,
configuration: { tasks: [], instances: [], globalVariables: [] },
}
}

describe('generateGraphicalPou — polymorphic conversion call resolution (issue #944)', () => {
it('emits REAL_TO_INT, not the bare TO_INT shorthand, for a REAL local wired into a TO_INT block', () => {
const pou: TranspilePou = {
name: 'main',
pouType: 'program',
interface: {
variables: [
{ name: 'O_R', type: { definition: 'base-type', value: 'REAL' }, class: 'local' },
{ name: 'OFFICE_TEMP', type: { definition: 'base-type', value: 'INT' }, class: 'local' },
],
},
body: {
language: 'fbd',
value: {
rung: {
nodes: [
inputVariableNode('in1', 'O_R'),
conversionBlockNode('blk1', 'TO_INT', '6913197'),
outputVariableNode('out1', 'OFFICE_TEMP'),
],
edges: [
{ id: 'e1', source: 'in1', target: 'blk1', targetHandle: 'IN' },
{ id: 'e2', source: 'blk1', target: 'out1', sourceHandle: 'OUT' },
],
},
},
},
}

const chunks = generateGraphicalPou(pou, emptyProject([pou]))
const text = chunks.map((c) => c[0]).join('')

expect(text).toContain('REAL_TO_INT(O_R)')
expect(text).not.toMatch(/:=\s*TO_INT\(/)
// The output temp's declared type must also resolve off ANY (the
// sibling defect PR #854 fixed) — belt-and-suspenders check that
// this change didn't regress that.
expect(text).toMatch(/_TMP_TO_INT6913197_OUT\s*:\s*INT/)
})

it('resolves the source type from a project global variable when the POU has no matching local', () => {
const pou: TranspilePou = {
name: 'main',
pouType: 'program',
interface: {
variables: [{ name: 'OFFICE_TEMP', type: { definition: 'base-type', value: 'INT' }, class: 'local' }],
},
body: {
language: 'fbd',
value: {
rung: {
nodes: [
inputVariableNode('in1', 'G_TEMP'),
conversionBlockNode('blk1', 'TO_INT', '42'),
outputVariableNode('out1', 'OFFICE_TEMP'),
],
edges: [
{ id: 'e1', source: 'in1', target: 'blk1', targetHandle: 'IN' },
{ id: 'e2', source: 'blk1', target: 'out1', sourceHandle: 'OUT' },
],
},
},
},
}
const project = emptyProject([pou])
project.configuration.globalVariables.push({
name: 'G_TEMP',
type: { definition: 'base-type', value: 'REAL' },
class: 'external',
})

const chunks = generateGraphicalPou(pou, project)
const text = chunks.map((c) => c[0]).join('')

expect(text).toContain('REAL_TO_INT(G_TEMP)')
})

it('does not index derived variable types as conversion sources', () => {
const pou: TranspilePou = {
name: 'main',
pouType: 'program',
interface: {
variables: [
{ name: 'DERIVED_TEMP', type: { definition: 'derived', value: 'REAL_ALIAS' }, class: 'local' },
{ name: 'OFFICE_TEMP', type: { definition: 'base-type', value: 'INT' }, class: 'local' },
],
},
body: {
language: 'fbd',
value: {
rung: {
nodes: [
inputVariableNode('in1', 'DERIVED_TEMP'),
conversionBlockNode('blk1', 'TO_INT', '43'),
outputVariableNode('out1', 'OFFICE_TEMP'),
],
edges: [
{ id: 'e1', source: 'in1', target: 'blk1', targetHandle: 'IN' },
{ id: 'e2', source: 'blk1', target: 'out1', sourceHandle: 'OUT' },
],
},
},
},
}

const chunks = generateGraphicalPou(pou, emptyProject([pou]))
const text = chunks.map((c) => c[0]).join('')

expect(text).toContain('TO_INT(DERIVED_TEMP)')
expect(text).not.toContain('REAL_ALIAS_TO_INT(DERIVED_TEMP)')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ function collectBlockSignatures(project: TranspileProject): Map<string, BlockInf
// GetVariableType + GetBlockType ports (PLCGenerator.py:786-817, PLCControler.py:1288-1335)
function buildTypeContext(pou: TranspilePou, project: TranspileProject): TypeContext {
const interfaceTypes = new Map<string, string>()
for (const v of project.configuration.globalVariables) {
interfaceTypes.set(v.name, getTypeAsText(v))
}
for (const v of pou.interface?.variables ?? []) {
interfaceTypes.set(v.name, getTypeAsText(v))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { resolveConversionFunctionName } from '../block-library'

describe('resolveConversionFunctionName', () => {
it('resolves a supported concrete conversion', () => {
expect(resolveConversionFunctionName('TO_INT', 'REAL')).toBe('REAL_TO_INT')
})

it('rejects an unknown TO_<TYPE> shorthand even if it matches the name pattern', () => {
expect(resolveConversionFunctionName('TO_FOO', 'REAL')).toBeNull()
})

it('rejects unsupported temporal conversions', () => {
expect(resolveConversionFunctionName('TO_TIME', 'DATE')).toBeNull()
})

it('supports BCD conversions only for unsigned integer types', () => {
expect(resolveConversionFunctionName('TO_UINT', 'BCD')).toBe('BCD_TO_UINT')
expect(resolveConversionFunctionName('TO_BCD', 'INT')).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,90 @@ export function blockInfosFromVariant(variant: unknown): BlockInfos | null {
usage: '',
}
}

/**
* Destination types of the IEC 61131-3 polymorphic conversion family
* (`TO_BOOL`, `TO_INT`, `TO_UINT`, …). Kept explicit so additions to the
* supported compiler conversion family remain visible in code review.
*/
export const TO_CONVERSION_TARGETS: ReadonlySet<string> = new Set([
'BCD',
'BOOL',
'BYTE',
'DATE',
'DINT',
'DT',
'DWORD',
'INT',
'LINT',
'LREAL',
'LWORD',
'REAL',
'SINT',
'STRING',
'TIME',
'TOD',
'UDINT',
'UINT',
'ULINT',
'USINT',
'WORD',
])

const TEMPORAL_TYPES: ReadonlySet<string> = new Set(['DATE', 'DT', 'TIME', 'TOD'])
const UNSIGNED_INTEGER_TYPES: ReadonlySet<string> = new Set(['UDINT', 'UINT', 'ULINT', 'USINT'])
const GENERAL_CONVERSION_TARGETS: ReadonlySet<string> = new Set([
'BYTE',
'DINT',
'DWORD',
'INT',
'LINT',
'LREAL',
'LWORD',
'REAL',
'SINT',
'STRING',
'UDINT',
'UINT',
'ULINT',
'USINT',
'WORD',
])

/**
* Resolve a polymorphic `TO_<TYPE>` block name (e.g. `TO_INT`) to the
* concrete, fully-qualified IEC 61131-3 conversion function (e.g.
* `REAL_TO_INT`) given the type of whatever is wired into its single
* input.
*
* IEC 61131-3 does not define a generic `TO_INT` — only the fully
* qualified `<SRC>_TO_<DST>` family exists. A block instance whose type name
* is still the generic shorthand at code-generation time hasn't been
* resolved to a concrete variant — a real ST/C compiler (matiec, STruC++)
* rejects the bare name as an undefined function.
*
* Returns `null` when `blockTypeName` isn't a recognised polymorphic
* shorthand, or when the supported compiler conversion family does not
* contain the source/destination pair. Callers should fall back to the
* original name so an unresolvable case still surfaces the same "undefined
* function" error it would have before, rather than silently emitting a
* different wrong name.
*/
export function resolveConversionFunctionName(blockTypeName: string, sourceType: string): string | null {
const match = blockTypeName.match(/^TO_([A-Z][A-Z0-9]*)$/)
if (match === null || !TO_CONVERSION_TARGETS.has(match[1])) return null
const source = sourceType.toUpperCase()
const destination = match[1]
if (!isSupportedConversionPair(source, destination)) return null
return `${source}_TO_${destination}`
}

function isSupportedConversionPair(source: string, destination: string): boolean {
if (destination === 'BCD') return UNSIGNED_INTEGER_TYPES.has(source)
if (source === 'BCD') return UNSIGNED_INTEGER_TYPES.has(destination)
if (destination === 'DATE' && source === 'DATE_AND_TIME') return true
if (!TO_CONVERSION_TARGETS.has(source) || source === destination) return false
if (GENERAL_CONVERSION_TARGETS.has(destination)) return true
if (destination === 'BOOL') return !TEMPORAL_TYPES.has(source)
return TEMPORAL_TYPES.has(destination) && !TEMPORAL_TYPES.has(source)
}
Loading