nodalis-compiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +134 -0
  2. package/package.json +59 -0
  3. package/src/compilers/CPPCompiler.js +272 -0
  4. package/src/compilers/Compiler.js +108 -0
  5. package/src/compilers/JSCompiler.js +293 -0
  6. package/src/compilers/iec-parser/parser.js +4254 -0
  7. package/src/compilers/st-parser/expressionConverter.js +155 -0
  8. package/src/compilers/st-parser/gcctranspiler.js +237 -0
  9. package/src/compilers/st-parser/jstranspiler.js +254 -0
  10. package/src/compilers/st-parser/parser.js +367 -0
  11. package/src/compilers/st-parser/tokenizer.js +78 -0
  12. package/src/compilers/support/generic/json.hpp +25526 -0
  13. package/src/compilers/support/generic/modbus.cpp +378 -0
  14. package/src/compilers/support/generic/modbus.h +124 -0
  15. package/src/compilers/support/generic/nodalis.cpp +421 -0
  16. package/src/compilers/support/generic/nodalis.h +798 -0
  17. package/src/compilers/support/generic/opcua.cpp +267 -0
  18. package/src/compilers/support/generic/opcua.h +50 -0
  19. package/src/compilers/support/generic/open62541.c +151897 -0
  20. package/src/compilers/support/generic/open62541.h +50357 -0
  21. package/src/compilers/support/jint/nodalis/Nodalis.sln +28 -0
  22. package/src/compilers/support/jint/nodalis/NodalisEngine/ModbusClient.cs +200 -0
  23. package/src/compilers/support/jint/nodalis/NodalisEngine/NodalisEngine.cs +817 -0
  24. package/src/compilers/support/jint/nodalis/NodalisEngine/NodalisEngine.csproj +16 -0
  25. package/src/compilers/support/jint/nodalis/NodalisEngine/OPCClient.cs +172 -0
  26. package/src/compilers/support/jint/nodalis/NodalisEngine/OPCServer.cs +275 -0
  27. package/src/compilers/support/jint/nodalis/NodalisPLC/NodalisPLC.csproj +19 -0
  28. package/src/compilers/support/jint/nodalis/NodalisPLC/Program.cs +197 -0
  29. package/src/compilers/support/jint/nodalis/NodalisPLC/bootstrap.bat +5 -0
  30. package/src/compilers/support/jint/nodalis/NodalisPLC/bootstrap.sh +5 -0
  31. package/src/compilers/support/jint/nodalis/build.bat +25 -0
  32. package/src/compilers/support/jint/nodalis/build.sh +31 -0
  33. package/src/compilers/support/nodejs/IOClient.js +110 -0
  34. package/src/compilers/support/nodejs/modbus.js +115 -0
  35. package/src/compilers/support/nodejs/nodalis.js +662 -0
  36. package/src/compilers/support/nodejs/opcua.js +194 -0
  37. package/src/nodalis.js +174 -0
@@ -0,0 +1,155 @@
1
+ /* eslint-disable curly */
2
+ /* eslint-disable eqeqeq */
3
+ // Copyright [2025] Nathan Skipper
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+
17
+
18
+ /**
19
+ * @description Expression Converter
20
+ * @author Nathan Skipper, MTI
21
+ * @version 1.0.2
22
+ * @copyright Apache 2.0
23
+ */
24
+
25
+ /**
26
+ * Converts an ST expression to be more understable to JS and C++
27
+ * @param {Array | string} expr An array of tokens or a string representing the expression.
28
+ * @param {boolean} isjsfb Expresses whether this expression is within a JS function block.
29
+ * @param {string[]} jsfbVars An array of variable names defined in the JS function block.
30
+ * @returns {string} Returns a converted expression.
31
+ */
32
+ export function convertExpression(expr, isjsfb = false, jsfbVars = [], isjs=false) {
33
+ if (Array.isArray(expr)) {
34
+ if (!isjsfb) {
35
+ expr = expr.join(" ");
36
+ } else {
37
+ let jsexpr = "";
38
+ expr.forEach((e) => {
39
+ let ev = e.split(".")[0];
40
+ if (jsfbVars.includes(ev)) {
41
+ ev = "this." + e;
42
+ }
43
+ jsexpr += (jsexpr ? " " : "") + ev;
44
+ });
45
+ expr = jsexpr;//.replace(/([^\s])/g, ' $1 ').replace(/\s+/g, ' ').trim(); // ✨ ensure spacing
46
+ }
47
+ }
48
+
49
+ let results = expr
50
+ .replace(/\bAND\b/gi, '&&')
51
+ .replace(/\bOR\b/gi, '||')
52
+ .replace(/\bNOT\b/gi, '!')
53
+ .replace(/\bMOD\b/gi, '%')
54
+ .replace(/\bDIV\b/gi, '/')
55
+ .replace(/<>/g, '!=')
56
+ .replace(/:=/g, '=')
57
+ .replace(/\bTRUE\b/gi, 'true')
58
+ .replace(/\bFALSE\b/gi, 'false')
59
+ .replace(/\b(?<![><!])=(?!=)/g, '=='); // ✅ fix assignment/comparison
60
+
61
+ const tokens = results.split(/\s+/);
62
+ for (let i = 0; i < tokens.length; i++) {
63
+ if (tokens[i] === '=' &&
64
+ tokens[i - 1] !== '<' &&
65
+ tokens[i - 1] !== '>' &&
66
+ tokens[i - 1] !== '!' &&
67
+ tokens[i + 1] !== '='
68
+ ) {
69
+ tokens[i] = '==';
70
+ }
71
+ }
72
+ results = tokens.join(' ');
73
+ // Replace %I/Q/M references
74
+ const parts = results.split(/\s+/);
75
+ results = parts.map(e => {
76
+ // Don't touch raw address reads
77
+ if (/^%[IQM][XBWDL]?\d+(\.\d+)?$/i.test(e)) return getReadAddressExpression(e);
78
+
79
+ // Don't wrap literals or operators
80
+ if (/^(true|false|null|\d+|!|&&|\|\||==|!=|[<>=+\-*/()])$/i.test(e)) return e;
81
+
82
+ // Don't wrap known function expressions (e.g., getBit)
83
+ if (/^getBit\(/.test(e)) return e;
84
+
85
+ // Don't wrap dot-bit references already processed
86
+ if (/^&?[A-Za-z_]\w*\.\d+$/.test(e)) return e;
87
+
88
+ // Otherwise, wrap in resolve()
89
+ if(isjs)
90
+ return `resolve(${e})`;
91
+ else return e;
92
+ }).join(' ');
93
+ if (results.indexOf("read") === -1) {
94
+ results = results.replace(/\b(?!%)(([A-Za-z_]\w*)\.(\d+))\b/g, (_, full, base, bit) => {
95
+ return `getBit(&${base}, ${bit})`;
96
+ });
97
+ }
98
+
99
+ return results;
100
+ }
101
+
102
+
103
+ /**
104
+ *
105
+ * @param {string} addr
106
+ * @returns
107
+ */
108
+ export function getReadAddressExpression(addr){
109
+ var result = `readDWord("${addr}")`;
110
+ try{
111
+ if(addr.indexOf(".")){
112
+ result = `readBit("${addr}")`;
113
+ }
114
+ else{
115
+ var width = addr.substring(2, 3).toUpperCase();
116
+ switch(width){
117
+ case "X":
118
+ result = `readByte("${addr}")`;
119
+ break;
120
+ case "W":
121
+ `readWord("${addr}")`;
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ catch(e){
127
+ console.error(e);
128
+ }
129
+ return result;
130
+ }
131
+
132
+ export function getWriteAddressExpression(addr, value){
133
+ var result = `writeDWord("${addr}", ${value})`;
134
+ try{
135
+ if(addr.indexOf(".") > -1){
136
+ result = `writeBit("${addr}", ${value})`;
137
+ }
138
+ else{
139
+ var width = addr.substring(2, 3).toUpperCase();
140
+ switch(width){
141
+ case "X":
142
+ result = `writeByte("${addr}", ${value})`;
143
+ break;
144
+ case "W":
145
+ `writeWord("${addr}", ${value})`;
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ catch(e){
151
+ console.error(e);
152
+ }
153
+ return result;
154
+ }
155
+
@@ -0,0 +1,237 @@
1
+ /* eslint-disable curly */
2
+ /* eslint-disable eqeqeq */
3
+ // Copyright [2025] Nathan Skipper
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+
17
+
18
+ /**
19
+ * @description ANSI CPP Transpiler
20
+ * @author Nathan Skipper, MTI
21
+ * @version 1.0.2
22
+ * @copyright Apache 2.0
23
+ */
24
+
25
+ import { convertExpression } from './expressionConverter.js';
26
+ import { getWriteAddressExpression } from './expressionConverter.js';
27
+
28
+ /**
29
+ * Converts the tokenized ST code to ANSCII C++.
30
+ * @param {{body: {type: string, name: string, varSections: [], statements: []}}[]} ast The tokenized code.
31
+ * @returns {string} The transpiled code.
32
+ */
33
+ export function transpile(ast) {
34
+ const lines = [];
35
+
36
+ for (const block of ast.body) {
37
+ switch (block.type) {
38
+ case 'GlobalVars':
39
+ lines.push('// Global variable declarations');
40
+ lines.push(...declareVars(block.variables));
41
+ break;
42
+ case 'ProgramDeclaration':
43
+ lines.push(`void ${block.name}() { //PROGRAM:${block.name}`);
44
+ lines.push(...declareVars(block.varSections));
45
+ lines.push(...transpileStatements(block.statements));
46
+ lines.push('}');
47
+ break;
48
+
49
+ case 'FunctionDeclaration':
50
+ lines.push(`${mapType(block.returnType)} ${block.name}() { //FUNCTION:${block.name}`);
51
+ lines.push(...declareVars(block.varSections));
52
+ lines.push(...transpileStatements(block.statements));
53
+ lines.push('}');
54
+
55
+ for(var x = 0; x < lines.length; x++){
56
+ var l = lines[x];
57
+ if(l.indexOf(`${block.name} =`) > -1){
58
+ lines[x] = l.replace(`${block.name} =`, "return");
59
+ }
60
+ }
61
+
62
+ break;
63
+
64
+ case 'FunctionBlockDeclaration':
65
+ lines.push(`class ${block.name} {//FUNCTION_BLOCK:${block.name}`);
66
+ lines.push('public:');
67
+ //for (const v of block.varSections) {
68
+ lines.push(...declareVars(block.varSections));
69
+ //}
70
+ lines.push(' void operator()() {');
71
+ lines.push(...transpileStatements(block.statements).map(line => ` ${line}`));
72
+ lines.push(' }');
73
+ lines.push('};');
74
+ break;
75
+ }
76
+ lines.push('');
77
+ }
78
+
79
+ return lines.join('\n');
80
+ }
81
+ /**
82
+ * Converts a single statement to C++
83
+ * @param {{type: string, left: string, right: string, condition:string[], elseIfBlocks: [], elseBlock: [], body: []}} stmt The tokenized statement to convert.
84
+ * @returns {string} the converted statement.
85
+ */
86
+ function mapStatement(stmt){
87
+ try{
88
+ switch (stmt.type) {
89
+ case 'ASSIGN': {
90
+ const left = stmt.left;
91
+ const rightExpr = convertExpression(stmt.right);
92
+ if (isIOAddress(left)) {
93
+ return getWriteAddressExpression(left, rightExpr) + ";";
94
+ } else if (isBitSelector(left)) {
95
+ const [varName, bitIndex] = left.split('.');
96
+ return `setBit(&${varName}, ${bitIndex}, ${rightExpr});`;
97
+ }
98
+ return `${left} = ${rightExpr};`;
99
+ }
100
+
101
+ case 'IF': {
102
+ const cond = convertExpression(Array.isArray(stmt.condition) ? stmt.condition.join(' ') : stmt.condition);
103
+ const lines = [];
104
+
105
+ lines.push(`if (${cond}) {`);
106
+ lines.push(...transpileStatements(stmt.thenBlock).map(s => ` ${s}`));
107
+ lines.push(`}`);
108
+
109
+ if (stmt.elseIfBlocks && stmt.elseIfBlocks.length > 0) {
110
+ for (const elif of stmt.elseIfBlocks) {
111
+ const elifCond = convertExpression(Array.isArray(elif.condition) ? elif.condition.join(' ') : elif.condition);
112
+ lines.push(`else if (${elifCond}) {`);
113
+ lines.push(...transpileStatements(elif.block).map(s => ` ${s}`));
114
+ lines.push(`}`);
115
+ }
116
+ }
117
+
118
+ if (stmt.elseBlock && stmt.elseBlock.length > 0) {
119
+ lines.push(`else {`);
120
+ lines.push(...transpileStatements(stmt.elseBlock).map(s => ` ${s}`));
121
+ lines.push(`}`);
122
+ }
123
+
124
+ return lines;
125
+ }
126
+
127
+ case 'WHILE':
128
+ const wcond = convertExpression(Array.isArray(stmt.condition) ? stmt.condition.join(' ') : stmt.condition);
129
+ return [
130
+ `while (${wcond}) {`,
131
+ ...transpileStatements(stmt.body)?.map(s => ` ${s}`),
132
+ `}`
133
+ ];
134
+
135
+ case 'FOR':
136
+ return [
137
+ `for (int ${stmt.variable} = ${stmt.start}; ${stmt.variable} <= ${stmt.end}; ${stmt.variable} += ${stmt.step}) {`,
138
+ ...transpileStatements(stmt.body)?.map(s => ` ${s}`),
139
+ `}`
140
+ ];
141
+ case "CALL":
142
+ return [stmt.name + "();"];
143
+ default:
144
+ return [`// unsupported: ${stmt.type}`];
145
+ }
146
+ }
147
+ catch(e){
148
+ console.error(e + "\n" + JSON.stringify(stmt));
149
+ }
150
+ return "// uncompilable statement " + JSON.stringify(stmt);
151
+ }
152
+
153
+ /**
154
+ * Transpiles an array of statements.
155
+ * @param {{type: string, left: string, right: string, condition:string[], elseIfBlocks: [], elseBlock: [], body: []}[]} statements The statements to transpile.
156
+ * @returns {string[]} Returns an array of transpiled statements.
157
+ */
158
+ function transpileStatements(statements) {
159
+ return statements?.flatMap(mapStatement);
160
+ }
161
+
162
+ /**
163
+ * Creates a transpiled section of declared variables.
164
+ * @param {{type: string, address: string, initialValue: string, sectionType: string}[]} varSections An array of variable tokens.
165
+ * @returns {string[]} An array of declaration statements.
166
+ */
167
+ function declareVars(varSections) {
168
+ return varSections.map(v => {
169
+ var cleanedType = v.type.trim().toUpperCase();
170
+ var gv = "";
171
+ const isFunctionBlockType = !mapType(cleanedType) || mapType(cleanedType) === 'auto';
172
+ let init = "";
173
+ cleanedType = mapType(cleanedType);
174
+ if(v.address){
175
+ cleanedType = "RefVar<" + cleanedType + ">";
176
+ var addr = v.address;
177
+ if(!addr.startsWith("%")) addr = "%" + addr;
178
+ init = `("${addr}")`
179
+ }
180
+ else if (v.initialValue !== undefined && v.initialValue !== null) {
181
+ init = ` = ${v.initialValue}`;
182
+ }
183
+ if (v.sectionType==='VAR' && isFunctionBlockType) {
184
+ return `static ${v.type} ${v.name};`; // assume Function Block type
185
+ }
186
+ return `${cleanedType} ${v.name}${init};${gv}`;
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Maps a structured text type to a C++ type.
192
+ * @param {string} type The ST type to map
193
+ * @returns {string} Returns a string representing the C++ equivalent for the structured text type.
194
+ */
195
+ export function mapType(type) {
196
+ const types = {
197
+ 'BOOL': 'bool',
198
+ 'BYTE': 'uint8_t',
199
+ 'WORD': 'uint16_t',
200
+ 'DWORD': 'uint32_t',
201
+ 'LWORD': 'uint64_t',
202
+ 'SINT': 'int8_t',
203
+ 'INT': 'int16_t',
204
+ 'DINT': 'int32_t',
205
+ 'LINT': 'int64_t',
206
+ 'USINT': 'uint8_t',
207
+ 'UINT': 'uint16_t',
208
+ 'UDINT': 'uint32_t',
209
+ 'ULINT': 'uint64_t',
210
+ 'REAL': 'float',
211
+ 'LREAL': 'double',
212
+ 'TIME': 'uint32_t',
213
+ 'DATE': 'std::string',
214
+ 'TIME_OF_DAY': 'std::string',
215
+ 'DATE_AND_TIME': 'std::string',
216
+ 'STRING': 'std::string',
217
+ 'WSTRING': 'std::wstring'
218
+ };
219
+ return types[type.trim().toUpperCase()] || 'auto';
220
+ }
221
+
222
+ /**
223
+ * Determins whether the expression is an address reference with a bit selector.
224
+ * @param {string} expr The expression to evaluate.
225
+ * @returns Returns true if the expression has a bit selector.
226
+ */
227
+ function isBitSelector(expr) {
228
+ return typeof expr === 'string' && /^[A-Za-z_]\w*\.\d+$/.test(expr);
229
+ }
230
+ /**
231
+ * Determines whether the expression is an address reference.
232
+ * @param {string} expr The expression to evaluate.
233
+ * @returns Returns true if the expression is an address reference.
234
+ */
235
+ function isIOAddress(expr) {
236
+ return typeof expr === 'string' && /^%[IQM]/i.test(expr);
237
+ }
@@ -0,0 +1,254 @@
1
+ /* eslint-disable curly */
2
+ /* eslint-disable eqeqeq */
3
+ // Copyright [2025] Nathan Skipper
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+
17
+
18
+ /**
19
+ * @description Javascript Transpiler
20
+ * @author Nathan Skipper, MTI
21
+ * @version 1.0.2
22
+ * @copyright Apache 2.0
23
+ */
24
+
25
+ import { convertExpression, getWriteAddressExpression } from './expressionConverter.js';
26
+ let fbVars = [];
27
+ let refVars = [];
28
+ /**
29
+ * Converts the tokenized ST code to Javascript.
30
+ * @param {{body: {type: string, name: string, varSections: [], statements: []}}[]} ast The tokenized code.
31
+ * @returns {string} The transpiled code.
32
+ */
33
+ export function transpile(ast) {
34
+ const lines = [];
35
+
36
+ for (const block of ast.body) {
37
+ switch (block.type) {
38
+ case 'GlobalVars':
39
+ lines.push('// Global variable declarations');
40
+ lines.push(...declareVars(block.variables));
41
+ break;
42
+
43
+ case 'ProgramDeclaration':
44
+ lines.push(`export function ${block.name}() { // PROGRAM:${block.name}`);
45
+ lines.push(...declareVars(block.varSections),false, block.name);
46
+ lines.push(...transpileStatements(block.statements));
47
+ lines.push('}');
48
+ break;
49
+
50
+ case 'FunctionDeclaration':
51
+ lines.push(`export function ${block.name}() { // FUNCTION:${block.name}`);
52
+ lines.push(...declareVars(block.varSections, false, block.name));
53
+ lines.push(...transpileStatements(block.statements));
54
+
55
+ for (let i = 0; i < lines.length; i++) {
56
+ if (lines[i].includes(`${block.name} =`)) {
57
+ lines[i] = lines[i].replace(`${block.name} =`, 'return');
58
+ }
59
+ }
60
+
61
+ lines.push('}');
62
+ break;
63
+
64
+ case 'FunctionBlockDeclaration':
65
+ lines.push(`export class ${block.name} { // FUNCTION_BLOCK:${block.name}`);
66
+ lines.push(' constructor() {');
67
+ lines.push(...declareVars(block.varSections, true, block.name).map(line => ` ${line}`));
68
+ lines.push(' }');
69
+ lines.push(' call() {');
70
+ lines.push(...transpileStatements(block.statements, true).map(line => ` ${line}`));
71
+ lines.push(' }');
72
+ lines.push('}');
73
+ break;
74
+ }
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+
79
+ /**
80
+ * Converts a single statement to Javascript
81
+ * @param {{type: string, left: string, right: string, condition:string[], elseIfBlocks: [], elseBlock: [], body: []}} stmt The tokenized statement to convert.
82
+ * @returns {string} the converted statement.
83
+ */
84
+ function mapStatement(stmt, infb = false) {
85
+ try {
86
+ switch (stmt.type) {
87
+ case 'ASSIGN': {
88
+ let left = stmt.left;
89
+ var e = left.split(".")[0];
90
+ if(infb && fbVars.includes(e)) {
91
+ left = "this." + left;
92
+ }
93
+
94
+ const rightExpr = convertExpression(stmt.right, infb, fbVars, true);
95
+
96
+ if (isIOAddress(left)) {
97
+ return getWriteAddressExpression(left, rightExpr) + ";";
98
+ } else if (isBitSelector(left)) {
99
+ const [varName, bitIndex] = left.split('.');
100
+ return `setBit(${varName}, ${bitIndex}, ${rightExpr});`;
101
+ }
102
+
103
+ return `${left} = ${rightExpr};`;
104
+ }
105
+
106
+ case 'IF': {
107
+ const cond = convertExpression(stmt.condition, infb, fbVars,true);
108
+ const lines = [];
109
+
110
+ lines.push(`if (${cond}) {`);
111
+ lines.push(...transpileStatements(stmt.thenBlock, infb).map(s => ` ${s}`));
112
+ lines.push(`}`);
113
+
114
+ if (stmt.elseIfBlocks?.length) {
115
+ for (const elif of stmt.elseIfBlocks) {
116
+ const elifCond = convertExpression(elif.condition, infb, fbVars,true);
117
+ lines.push(`else if (${elifCond}) {`);
118
+ lines.push(...transpileStatements(elif.block, infb).map(s => ` ${s}`));
119
+ lines.push('}');
120
+ }
121
+ }
122
+
123
+ if (stmt.elseBlock?.length) {
124
+ lines.push('else {');
125
+ lines.push(...transpileStatements(stmt.elseBlock, infb).map(s => ` ${s}`));
126
+ lines.push('}');
127
+ }
128
+
129
+ return lines;
130
+ }
131
+
132
+ case 'WHILE': {
133
+ const cond = convertExpression(stmt.condition, infb, fbVars,true);
134
+ return [
135
+ `while (${cond}) {`,
136
+ ...transpileStatements(stmt.body, infb).map(s => ` ${s}`),
137
+ `}`
138
+ ];
139
+ }
140
+
141
+ case 'FOR':
142
+ return [
143
+ `for (let ${stmt.variable} = ${stmt.from}; ${stmt.variable} <= ${stmt.to}; ${stmt.variable} += ${stmt.step}) {`,
144
+ ...transpileStatements(stmt.body, infb).map(s => ` ${s}`),
145
+ `}`
146
+ ];
147
+
148
+ case 'REPEAT': {
149
+ const cond = convertExpression(stmt.condition, infb, fbVars,true);
150
+ return [
151
+ `do {`,
152
+ ...transpileStatements(stmt.body, infb).map(s => ` ${s}`),
153
+ `} while (!(${cond}));`
154
+ ];
155
+ }
156
+
157
+ case 'CALL':
158
+ let ext = "";
159
+ if(infb && fbVars.includes(stmt.name)){
160
+ ext = "this.";
161
+ }
162
+ return [`${ext}${stmt.name}.call();`];
163
+
164
+ default:
165
+ return [`// Unsupported statement type: ${stmt.type}`];
166
+ }
167
+ } catch (e) {
168
+ console.error("Error transpiling statement", stmt, e);
169
+ return [`// Failed to transpile: ${JSON.stringify(stmt)}`];
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Transpiles an array of statements.
175
+ * @param {{type: string, left: string, right: string, condition:string[], elseIfBlocks: [], elseBlock: [], body: []}[]} statements The statements to transpile.
176
+ * @returns {string[]} Returns an array of transpiled statements.
177
+ */
178
+ function transpileStatements(statements, infb=false) {
179
+ return statements?.flatMap((stmt) => mapStatement(stmt, infb));
180
+ }
181
+
182
+ /**
183
+ * Creates a transpiled section of declared variables.
184
+ * @param {{type: string, address: string, initialValue: string, sectionType: string}[]} varSections An array of variable tokens.
185
+ * @returns {string[]} An array of declaration statements.
186
+ */
187
+ function declareVars(varSections, infb = false, blockName = "") {
188
+ if(infb){
189
+ fbVars = [];
190
+ }
191
+ return varSections.map(v => {
192
+ const isFunctionBlock = !mapType(v.type) || mapType(v.type) === 'any';
193
+ const decl = infb ? "this." : "let ";
194
+ if(infb) fbVars.push(v.name);
195
+ if (v.address) {
196
+ const addr = v.address.startsWith('%') ? v.address : '%' + v.address;
197
+ refVars.push(v.name);
198
+ return `${decl}${v.name} = createReference("${addr}");`;
199
+ }
200
+
201
+ const fullVarName = blockName ? `${blockName}.${v.name}` : v.name;
202
+
203
+ const initValue = (v.initialValue !== undefined && v.initialValue !== null)
204
+ ? ` = ${v.initialValue}`
205
+ : isFunctionBlock ? ` = newStatic("${fullVarName}", ${v.type})` : infb ? " = null" : "";
206
+
207
+ return `${decl}${v.name}${initValue};`;
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Determines whether the expression is an address reference.
213
+ * @param {string} expr The expression to evaluate.
214
+ * @returns Returns true if the expression is an address reference.
215
+ */
216
+ function isIOAddress(expr) {
217
+ return typeof expr === 'string' && /^%[IQM]/i.test(expr);
218
+ }
219
+
220
+ /**
221
+ * Determins whether the expression is an address reference with a bit selector.
222
+ * @param {string} expr The expression to evaluate.
223
+ * @returns Returns true if the expression has a bit selector.
224
+ */
225
+ function isBitSelector(expr) {
226
+ return typeof expr === 'string' && /^[A-Za-z_]\w*\.\d+$/.test(expr);
227
+ }
228
+
229
+ function mapType(type) {
230
+ const jsTypes = {
231
+ 'BOOL': 'boolean',
232
+ 'BYTE': 'number',
233
+ 'WORD': 'number',
234
+ 'DWORD': 'number',
235
+ 'LWORD': 'number',
236
+ 'SINT': 'number',
237
+ 'INT': 'number',
238
+ 'DINT': 'number',
239
+ 'LINT': 'number',
240
+ 'USINT': 'number',
241
+ 'UINT': 'number',
242
+ 'UDINT': 'number',
243
+ 'ULINT': 'number',
244
+ 'REAL': 'number',
245
+ 'LREAL': 'number',
246
+ 'TIME': 'number',
247
+ 'DATE': 'string',
248
+ 'TIME_OF_DAY': 'string',
249
+ 'DATE_AND_TIME': 'string',
250
+ 'STRING': 'string',
251
+ 'WSTRING': 'string'
252
+ };
253
+ return jsTypes[type?.trim().toUpperCase()] || 'any';
254
+ }