nodalis-compiler 1.0.28 → 1.0.30

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.30] 2026-03-30
4
+ - Fixed issue with boolean assignments in C++
5
+
6
+ ## [1.0.29] 2026-03-24
7
+ - Fixed issues with bunding ST folder and reporting errors.
8
+ - Improved support for loop structures.
9
+
3
10
  ## [1.0.28] 2026-03-23
4
11
  - Improved support for Arduino, fully tested Arduino compile -> program stack.
5
12
  - Added a command line interface for nodalis on Arduino to use in setting IP, read/write of bits, and map info.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodalis-compiler",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.",
5
5
  "icon": "nodalis.png",
6
6
  "main": "src/nodalis.js",
@@ -92,6 +92,7 @@ export class ArduinoCompiler extends Compiler {
92
92
  const sourceIsDirectory = sourcePathStat.isDirectory();
93
93
  const isStructuredTextLanguage = String(language || '').toUpperCase() === IECLanguage.STRUCTURED_TEXT;
94
94
  const directoryBundleMode = sourceIsDirectory && isStructuredTextLanguage && typeof resourceName === 'string' && resourceName.trim().length > 0;
95
+ let bundleFileLineMappings = [];
95
96
  let compilerConfig = {};
96
97
 
97
98
  const sourceDir = sourceIsDirectory ? sourcePath : path.dirname(sourcePath);
@@ -114,15 +115,20 @@ export class ArduinoCompiler extends Compiler {
114
115
  let bundleEntryProgram = '';
115
116
  if (directoryBundleMode) {
116
117
  this.cleanupStructuredTextBundleArtifacts(sourcePath);
117
- const { combinedSource, entryProgramName } = this.loadStructuredTextBundle(sourcePath, resourceName);
118
+ const { combinedSource, entryProgramName, fileLineMappings } = this.loadStructuredTextBundle(sourcePath, resourceName);
118
119
  sourceCode = combinedSource;
119
120
  bundleEntryProgram = entryProgramName;
121
+ bundleFileLineMappings = fileLineMappings;
120
122
  } else {
121
123
  sourceCode = fs.readFileSync(sourcePath, 'utf-8');
122
124
  }
123
125
  const sketchName = path.basename(path.resolve(outputPath));
124
126
  const inoFile = path.join(outputPath, `${sketchName}.ino`);
125
127
  const stFile = path.join(outputPath, directoryBundleMode ? 'nodalisplc.st' : `${filename}.st`);
128
+ if (directoryBundleMode) {
129
+ fs.mkdirSync(outputPath, { recursive: true });
130
+ fs.writeFileSync(stFile, sourceCode);
131
+ }
126
132
  if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml')) {
127
133
  if (typeof resourceName === 'undefined' || resourceName === null || resourceName.length === 0) {
128
134
  throw new Error('You must provide the resourceName option for an IEC project file.');
@@ -152,8 +158,8 @@ export class ArduinoCompiler extends Compiler {
152
158
  }
153
159
  }
154
160
 
155
- const parsed = parseStructuredText(sourceCode);
156
- const transpiledCode = transpile(parsed);
161
+ const parsed = this.parseStructuredTextWithBundleContext(parseStructuredText, sourceCode, bundleFileLineMappings);
162
+ const transpiledCode = this.transpileStructuredTextWithBundleContext(transpile, parsed, bundleFileLineMappings);
157
163
 
158
164
  let tasks = [];
159
165
  let programs = [];
@@ -285,7 +291,7 @@ void loop() {
285
291
 
286
292
  fs.mkdirSync(outputPath, { recursive: true });
287
293
  fs.writeFileSync(inoFile, inoCode);
288
- if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml') || directoryBundleMode) {
294
+ if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml')) {
289
295
  fs.writeFileSync(stFile, sourceCode);
290
296
  }
291
297
 
@@ -333,50 +339,6 @@ void loop() {
333
339
  }
334
340
  }
335
341
 
336
- loadStructuredTextBundle(sourcePath, resourceName) {
337
- const stFiles = this.listStructuredTextBundleFiles(sourcePath);
338
-
339
- if (stFiles.length === 0) {
340
- throw new Error(`No .st files found in source directory "${sourcePath}".`);
341
- }
342
-
343
- const normalizedResource = String(resourceName || '').trim();
344
- const candidateNames = new Set([
345
- normalizedResource,
346
- normalizedResource.toLowerCase(),
347
- normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
348
- ]);
349
-
350
- const entryFile = stFiles.find((file) => {
351
- const lower = file.toLowerCase();
352
- return candidateNames.has(file) || candidateNames.has(lower);
353
- });
354
-
355
- if (!entryFile) {
356
- throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
357
- }
358
-
359
- const orderedFiles = [
360
- ...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
361
- entryFile
362
- ];
363
-
364
- const combinedSource = orderedFiles
365
- .map((file) => fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim())
366
- .filter((text) => text.length > 0)
367
- .join('\n\n');
368
-
369
- const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
370
- const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
371
-
372
- return { combinedSource, entryProgramName };
373
- }
374
-
375
- extractFirstProgramName(sourceCode) {
376
- const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
377
- return match ? match[1] : null;
378
- }
379
-
380
342
  resolveArduinoFqbn(target, compilerConfig = {}) {
381
343
  const explicit = compilerConfig.arduino_fqbn || compilerConfig.arduinoFqbn;
382
344
  if (explicit && typeof explicit === 'string' && explicit.includes(':')) {
@@ -71,6 +71,7 @@ export class CPPCompiler extends Compiler {
71
71
  const sourceIsDirectory = sourcePathStat.isDirectory();
72
72
  const isStructuredTextLanguage = String(language || '').toUpperCase() === IECLanguage.STRUCTURED_TEXT;
73
73
  const directoryBundleMode = sourceIsDirectory && isStructuredTextLanguage && typeof resourceName === 'string' && resourceName.trim().length > 0;
74
+ let bundleFileLineMappings = [];
74
75
 
75
76
  ToolChain = { ...getDefaultToolchain() };
76
77
  const sourceDir = sourceIsDirectory ? sourcePath : path.dirname(sourcePath);
@@ -97,15 +98,20 @@ export class CPPCompiler extends Compiler {
97
98
 
98
99
  if (directoryBundleMode) {
99
100
  this.cleanupStructuredTextBundleArtifacts(sourcePath);
100
- const { combinedSource, entryProgramName } = this.loadStructuredTextBundle(sourcePath, resourceName);
101
+ const { combinedSource, entryProgramName, fileLineMappings } = this.loadStructuredTextBundle(sourcePath, resourceName);
101
102
  sourceCode = combinedSource;
102
103
  bundleEntryProgram = entryProgramName;
104
+ bundleFileLineMappings = fileLineMappings;
103
105
  } else {
104
106
  sourceCode = fs.readFileSync(sourcePath, 'utf-8');
105
107
  }
106
108
 
107
109
  const cppFile = path.join(outputPath, `${filename}.cpp`);
108
110
  const stFile = path.join(outputPath, directoryBundleMode ? 'nodalisplc.st' : `${filename}.st`);
111
+ if (directoryBundleMode) {
112
+ fs.mkdirSync(outputPath, { recursive: true });
113
+ fs.writeFileSync(stFile, sourceCode);
114
+ }
109
115
  if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){
110
116
  if(typeof resourceName === "undefined" || resourceName === null || resourceName.length === 0){
111
117
  throw new Error("You must provide the resourceName option for an IEC project file.");
@@ -134,8 +140,8 @@ export class CPPCompiler extends Compiler {
134
140
  throw new Error("No resource was found by the name " + resourceName + " or the resource could not be parsed.");
135
141
  }
136
142
  }
137
- const parsed = parseStructuredText(sourceCode);
138
- const transpiledCode = transpile(parsed);
143
+ const parsed = this.parseStructuredTextWithBundleContext(parseStructuredText, sourceCode, bundleFileLineMappings);
144
+ const transpiledCode = this.transpileStructuredTextWithBundleContext(transpile, parsed, bundleFileLineMappings);
139
145
 
140
146
  let tasks = [];
141
147
  let programs = [];
@@ -247,7 +253,7 @@ int main() {
247
253
 
248
254
  fs.mkdirSync(outputPath, { recursive: true });
249
255
  fs.writeFileSync(cppFile, cppCode);
250
- if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml") || directoryBundleMode){
256
+ if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){
251
257
  fs.writeFileSync(stFile, sourceCode);
252
258
  }
253
259
  const coreDir = path.resolve(__dirname + '/support/generic');
@@ -400,50 +406,6 @@ int main() {
400
406
  return null;
401
407
  }
402
408
 
403
- loadStructuredTextBundle(sourcePath, resourceName) {
404
- const stFiles = this.listStructuredTextBundleFiles(sourcePath);
405
-
406
- if (stFiles.length === 0) {
407
- throw new Error(`No .st files found in source directory "${sourcePath}".`);
408
- }
409
-
410
- const normalizedResource = String(resourceName || '').trim();
411
- const candidateNames = new Set([
412
- normalizedResource,
413
- normalizedResource.toLowerCase(),
414
- normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
415
- ]);
416
-
417
- const entryFile = stFiles.find((file) => {
418
- const lower = file.toLowerCase();
419
- return candidateNames.has(file) || candidateNames.has(lower);
420
- });
421
-
422
- if (!entryFile) {
423
- throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
424
- }
425
-
426
- const orderedFiles = [
427
- ...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
428
- entryFile
429
- ];
430
-
431
- const combinedSource = orderedFiles
432
- .map((file) => fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim())
433
- .filter((text) => text.length > 0)
434
- .join('\n\n');
435
-
436
- const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
437
- const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
438
-
439
- return { combinedSource, entryProgramName };
440
- }
441
-
442
- extractFirstProgramName(sourceCode) {
443
- const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
444
- return match ? match[1] : null;
445
- }
446
-
447
409
  resolveTarget(target) {
448
410
  if (!target || typeof target !== 'string') {
449
411
  throw new Error('You must provide a valid target (e.g., linux-x64, macos-arm64).');
@@ -91,6 +91,115 @@ export class Compiler {
91
91
  .map((entry) => entry.name);
92
92
  }
93
93
 
94
+ loadStructuredTextBundle(sourcePath, resourceName) {
95
+ const stFiles = this.listStructuredTextBundleFiles(sourcePath);
96
+
97
+ if (stFiles.length === 0) {
98
+ throw new Error(`No .st files found in source directory "${sourcePath}".`);
99
+ }
100
+
101
+ const normalizedResource = String(resourceName || '').trim();
102
+ const candidateNames = new Set([
103
+ normalizedResource,
104
+ normalizedResource.toLowerCase(),
105
+ normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
106
+ ]);
107
+
108
+ const entryFile = stFiles.find((file) => {
109
+ const lower = file.toLowerCase();
110
+ return candidateNames.has(file) || candidateNames.has(lower);
111
+ });
112
+
113
+ if (!entryFile) {
114
+ throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
115
+ }
116
+
117
+ const orderedFiles = [
118
+ ...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
119
+ entryFile
120
+ ];
121
+
122
+ let combinedSource = '';
123
+ let currentLine = 1;
124
+ const fileLineMappings = [];
125
+
126
+ for (const file of orderedFiles) {
127
+ const fileSource = fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim();
128
+ if (fileSource.length === 0) continue;
129
+
130
+ if (combinedSource.length > 0) {
131
+ combinedSource += '\n\n';
132
+ currentLine += 1;
133
+ }
134
+
135
+ const lineCount = fileSource.split('\n').length;
136
+ fileLineMappings.push({
137
+ file,
138
+ startLine: currentLine,
139
+ endLine: currentLine + lineCount - 1
140
+ });
141
+ combinedSource += fileSource;
142
+ currentLine += lineCount;
143
+ }
144
+
145
+ const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
146
+ const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
147
+
148
+ return { combinedSource, entryProgramName, fileLineMappings };
149
+ }
150
+
151
+ extractFirstProgramName(sourceCode) {
152
+ const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
153
+ return match ? match[1] : null;
154
+ }
155
+
156
+ annotateStructuredTextBundleError(error, fileLineMappings = []) {
157
+ if (!error?.line || !Array.isArray(fileLineMappings) || fileLineMappings.length === 0) {
158
+ return error;
159
+ }
160
+
161
+ const mapping = fileLineMappings.find(({ startLine, endLine }) => error.line >= startLine && error.line <= endLine);
162
+ if (!mapping) {
163
+ return error;
164
+ }
165
+
166
+ const localLine = error.line - mapping.startLine + 1;
167
+ const column = error.column ?? error.sourceLocation?.column ?? 1;
168
+ const baseMessage = String(error.message || 'Structured Text error').replace(
169
+ /\s+at line \d+, column \d+$/,
170
+ ''
171
+ );
172
+
173
+ error.bundleLine = error.line;
174
+ error.file = mapping.file;
175
+ error.line = localLine;
176
+ error.column = column;
177
+ error.sourceLocation = {
178
+ file: mapping.file,
179
+ line: localLine,
180
+ column,
181
+ bundleLine: error.bundleLine
182
+ };
183
+ error.message = `${baseMessage} in ${mapping.file} at line ${localLine}, column ${column} (bundle line ${error.bundleLine})`;
184
+ return error;
185
+ }
186
+
187
+ parseStructuredTextWithBundleContext(parseFn, sourceCode, fileLineMappings) {
188
+ try {
189
+ return parseFn(sourceCode);
190
+ } catch (error) {
191
+ throw this.annotateStructuredTextBundleError(error, fileLineMappings);
192
+ }
193
+ }
194
+
195
+ transpileStructuredTextWithBundleContext(transpileFn, parsed, fileLineMappings) {
196
+ try {
197
+ return transpileFn(parsed);
198
+ } catch (error) {
199
+ throw this.annotateStructuredTextBundleError(error, fileLineMappings);
200
+ }
201
+ }
202
+
94
203
  /** @returns {string[]} */
95
204
  get supportedLanguages() {
96
205
  throw new Error('supportedLanguages must be implemented by subclass.');
@@ -60,20 +60,26 @@ export class JSCompiler extends Compiler {
60
60
  const sourceIsDirectory = sourcePathStat.isDirectory();
61
61
  const isStructuredTextLanguage = String(language || '').toUpperCase() === IECLanguage.STRUCTURED_TEXT;
62
62
  const directoryBundleMode = sourceIsDirectory && isStructuredTextLanguage && typeof resourceName === 'string' && resourceName.trim().length > 0;
63
+ let bundleFileLineMappings = [];
63
64
  const sourceName = directoryBundleMode ? resourceName : sourcePath;
64
65
  const filename = path.basename(sourceName, path.extname(sourceName));
65
66
  let sourceCode = '';
66
67
  let bundleEntryProgram = '';
67
68
  if (directoryBundleMode) {
68
69
  this.cleanupStructuredTextBundleArtifacts(sourcePath);
69
- const { combinedSource, entryProgramName } = this.loadStructuredTextBundle(sourcePath, resourceName);
70
+ const { combinedSource, entryProgramName, fileLineMappings } = this.loadStructuredTextBundle(sourcePath, resourceName);
70
71
  sourceCode = combinedSource;
71
72
  bundleEntryProgram = entryProgramName;
73
+ bundleFileLineMappings = fileLineMappings;
72
74
  } else {
73
75
  sourceCode = fs.readFileSync(sourcePath, 'utf-8');
74
76
  }
75
77
  const jsFile = path.join(outputPath, `nodalisplc.js`);
76
78
  const stFile = path.join(outputPath, directoryBundleMode ? 'nodalisplc.st' : `${filename}.st`);
79
+ if (directoryBundleMode) {
80
+ fs.mkdirSync(outputPath, { recursive: true });
81
+ fs.writeFileSync(stFile, sourceCode);
82
+ }
77
83
  if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){
78
84
  if(typeof resourceName === "undefined" || resourceName === null || resourceName.length === 0){
79
85
  throw new Error("You must provide the resourceName option for an IEC project file.");
@@ -102,8 +108,8 @@ export class JSCompiler extends Compiler {
102
108
  throw new Error("No resource was found by the name " + resourceName + " or the resource could not be parsed.");
103
109
  }
104
110
  }
105
- const parsed = parseStructuredText(sourceCode);
106
- const transpiledCode = transpile(parsed);
111
+ const parsed = this.parseStructuredTextWithBundleContext(parseStructuredText, sourceCode, bundleFileLineMappings);
112
+ const transpiledCode = this.transpileStructuredTextWithBundleContext(transpile, parsed, bundleFileLineMappings);
107
113
 
108
114
  let tasks = [];
109
115
  let programs = [];
@@ -231,7 +237,7 @@ export function run(){
231
237
  if (this.isExecutableOutput()) {
232
238
  fs.mkdirSync(binDir, { recursive: true });
233
239
  }
234
- if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml") || directoryBundleMode){
240
+ if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){
235
241
  fs.writeFileSync(stFile, sourceCode);
236
242
  }
237
243
  if(target === "nodejs"){
@@ -316,50 +322,6 @@ export function run(){
316
322
 
317
323
  }
318
324
 
319
- JSCompiler.prototype.loadStructuredTextBundle = function (sourcePath, resourceName) {
320
- const stFiles = this.listStructuredTextBundleFiles(sourcePath);
321
-
322
- if (stFiles.length === 0) {
323
- throw new Error(`No .st files found in source directory "${sourcePath}".`);
324
- }
325
-
326
- const normalizedResource = String(resourceName || '').trim();
327
- const candidateNames = new Set([
328
- normalizedResource,
329
- normalizedResource.toLowerCase(),
330
- normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
331
- ]);
332
-
333
- const entryFile = stFiles.find((file) => {
334
- const lower = file.toLowerCase();
335
- return candidateNames.has(file) || candidateNames.has(lower);
336
- });
337
-
338
- if (!entryFile) {
339
- throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
340
- }
341
-
342
- const orderedFiles = [
343
- ...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
344
- entryFile
345
- ];
346
-
347
- const combinedSource = orderedFiles
348
- .map((file) => fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim())
349
- .filter((text) => text.length > 0)
350
- .join('\n\n');
351
-
352
- const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
353
- const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
354
-
355
- return { combinedSource, entryProgramName };
356
- };
357
-
358
- JSCompiler.prototype.extractFirstProgramName = function (sourceCode) {
359
- const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
360
- return match ? match[1] : null;
361
- };
362
-
363
325
  function writePackageJson(outputDir,plcname) {
364
326
  const pkg = {
365
327
  name: "nodalisplc",
@@ -144,6 +144,41 @@ function mapStatement(stmt){
144
144
  ...transpileStatements(stmt.body)?.map(s => ` ${s}`),
145
145
  `}`
146
146
  ];
147
+ case 'EXIT':
148
+ return ['break;'];
149
+ case 'REPEAT': {
150
+ const cond = convertExpression(Array.isArray(stmt.condition) ? stmt.condition.join(' ') : stmt.condition);
151
+ return [
152
+ 'do {',
153
+ ...transpileStatements(stmt.body)?.map(s => ` ${s}`),
154
+ `} while (!(${cond}));`
155
+ ];
156
+ }
157
+ case 'CASE': {
158
+ const expr = convertExpression(Array.isArray(stmt.expression) ? stmt.expression.join(' ') : stmt.expression);
159
+ const lines = [];
160
+
161
+ stmt.branches.forEach((branch, index) => {
162
+ const labels = String(branch.label || '')
163
+ .split(',')
164
+ .map((label) => label.trim())
165
+ .filter(Boolean);
166
+ const branchCondition = labels
167
+ .map((label) => `${expr} == ${convertExpression(label)}`)
168
+ .join(' || ');
169
+ lines.push(`${index === 0 ? 'if' : 'else if'} (${branchCondition}) {`);
170
+ lines.push(...transpileStatements(branch.body)?.map((s) => ` ${s}`));
171
+ lines.push('}');
172
+ });
173
+
174
+ if (stmt.elseBlock?.length) {
175
+ lines.push('else {');
176
+ lines.push(...transpileStatements(stmt.elseBlock)?.map((s) => ` ${s}`));
177
+ lines.push('}');
178
+ }
179
+
180
+ return lines;
181
+ }
147
182
  case "CALL": {
148
183
  const isFunctionBlockCall = isFunctionBlockInstance(stmt.name);
149
184
  const argParts = parseCallArguments(stmt.args || []);
@@ -234,6 +269,9 @@ function declareVars(varSections, inFunctionBlock = false) {
234
269
  else if (v.initialValue !== undefined && v.initialValue !== null) {
235
270
  init = ` = ${normalizeCppLiteral(v.initialValue)}`;
236
271
  }
272
+ else if (!isFunctionBlockType) {
273
+ init = '{}';
274
+ }
237
275
  if (isFunctionBlockType) {
238
276
  const functionBlockType = resolveFunctionBlockTypeName(rawType);
239
277
  const storagePrefix = (!inFunctionBlock && v.sectionType === 'VAR') ? 'static ' : '';
@@ -151,6 +151,9 @@ function mapStatement(stmt, infb = false) {
151
151
  `}`
152
152
  ];
153
153
 
154
+ case 'EXIT':
155
+ return ['break;'];
156
+
154
157
  case 'REPEAT': {
155
158
  const cond = convertExpression(stmt.condition, infb, fbVars,true);
156
159
  return [
@@ -160,6 +163,32 @@ function mapStatement(stmt, infb = false) {
160
163
  ];
161
164
  }
162
165
 
166
+ case 'CASE': {
167
+ const expr = convertExpression(stmt.expression, infb, fbVars, true);
168
+ const lines = [];
169
+
170
+ stmt.branches.forEach((branch, index) => {
171
+ const labels = String(branch.label || '')
172
+ .split(',')
173
+ .map((label) => label.trim())
174
+ .filter(Boolean);
175
+ const branchCondition = labels
176
+ .map((label) => `${expr} == ${convertExpression(label, infb, fbVars, true)}`)
177
+ .join(' || ');
178
+ lines.push(`${index === 0 ? 'if' : 'else if'} (${branchCondition}) {`);
179
+ lines.push(...transpileStatements(branch.body, infb).map(s => ` ${s}`));
180
+ lines.push('}');
181
+ });
182
+
183
+ if (stmt.elseBlock?.length) {
184
+ lines.push('else {');
185
+ lines.push(...transpileStatements(stmt.elseBlock, infb).map(s => ` ${s}`));
186
+ lines.push('}');
187
+ }
188
+
189
+ return lines;
190
+ }
191
+
163
192
  case 'CALL': {
164
193
  const targetName = resolveCallTarget(stmt.name, infb);
165
194
  const isFunctionBlockCall = isFunctionBlockInstance(stmt.name);
@@ -264,6 +264,11 @@ function parseStatement() {
264
264
  if (token.value.toUpperCase() === 'FOR') return parseFor();
265
265
  if (token.value.toUpperCase() === 'REPEAT') return parseRepeat();
266
266
  if (token.value.toUpperCase() === 'CASE') return parseCase();
267
+ if (token.value.toUpperCase() === 'EXIT') {
268
+ consume();
269
+ if (peek()?.value === ';') consume();
270
+ return withLoc({ type: 'EXIT' }, token);
271
+ }
267
272
 
268
273
  // Call statement like: Foo(...);
269
274
  if (token.type === 'IDENTIFIER' && peek(1)?.value === '(') {