nodalis-compiler 1.0.27 → 1.0.29
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 +8 -0
- package/package.json +1 -1
- package/src/compilers/ArduinoCompiler.js +55 -49
- package/src/compilers/CPPCompiler.js +10 -48
- package/src/compilers/Compiler.js +109 -0
- package/src/compilers/JSCompiler.js +10 -48
- package/src/compilers/st-parser/gcctranspiler.js +35 -0
- package/src/compilers/st-parser/jstranspiler.js +29 -0
- package/src/compilers/st-parser/parser.js +5 -0
- package/src/compilers/support/arduino/gpio.cpp +1 -0
- package/src/compilers/support/arduino/modbus.cpp +96 -19
- package/src/compilers/support/arduino/modbus.h +1 -1
- package/src/compilers/support/arduino/network_config.cpp +502 -0
- package/src/compilers/support/arduino/network_config.h +20 -0
- package/src/compilers/support/arduino/nodalis.cpp +119 -15
- package/src/compilers/support/arduino/nodalis.h +10 -0
- package/src/compilers/support/generic/gpio.cpp +9 -0
- package/src/compilers/support/generic/gpio.h +1 -0
- package/src/compilers/support/nodejs/gpio.js +3 -0
- package/src/programmers/ArduinoProgrammer.js +37 -1
- package/src/programmers/SSHProgrammer.js +50 -2
- package/src/programmers/utils.js +27 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.29] 2026-03-04
|
|
4
|
+
- Fixed issues with bunding ST folder and reporting errors.
|
|
5
|
+
- Improved support for loop structures.
|
|
6
|
+
|
|
7
|
+
## [1.0.28] 2026-03-23
|
|
8
|
+
- Improved support for Arduino, fully tested Arduino compile -> program stack.
|
|
9
|
+
- Added a command line interface for nodalis on Arduino to use in setting IP, read/write of bits, and map info.
|
|
10
|
+
|
|
3
11
|
## [1.0.27] 2026-03-20
|
|
4
12
|
- Corrected issues with SSH Programmer.
|
|
5
13
|
|
package/package.json
CHANGED
|
@@ -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
|
|
156
|
-
const transpiledCode = transpile
|
|
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 = [];
|
|
@@ -219,20 +225,62 @@ export class ArduinoCompiler extends Compiler {
|
|
|
219
225
|
|
|
220
226
|
const inoCode = `#include "nodalis.h"
|
|
221
227
|
#include <stdint.h>
|
|
228
|
+
#include <Ethernet.h>
|
|
222
229
|
#include "modbus.h"
|
|
230
|
+
#include "network_config.h"
|
|
223
231
|
|
|
224
232
|
NodalisModbusTcpServer modbusServer;
|
|
233
|
+
IPAddress localIp;
|
|
234
|
+
|
|
235
|
+
#if defined(LEDR)
|
|
236
|
+
const int NODALIS_HEARTBEAT_LED_PIN = LEDR;
|
|
237
|
+
#elif defined(LED_BUILTIN)
|
|
238
|
+
const int NODALIS_HEARTBEAT_LED_PIN = LED_BUILTIN;
|
|
239
|
+
#else
|
|
240
|
+
const int NODALIS_HEARTBEAT_LED_PIN = -1;
|
|
241
|
+
#endif
|
|
242
|
+
|
|
243
|
+
void nodalisHeartbeatTask() {
|
|
244
|
+
if (NODALIS_HEARTBEAT_LED_PIN < 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static uint64_t lastToggle = 0;
|
|
249
|
+
static bool ledState = false;
|
|
250
|
+
const uint64_t now = elapsed();
|
|
251
|
+
if (now - lastToggle < 500) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
lastToggle = now;
|
|
256
|
+
ledState = !ledState;
|
|
257
|
+
digitalWrite(NODALIS_HEARTBEAT_LED_PIN, ledState ? HIGH : LOW);
|
|
258
|
+
}
|
|
225
259
|
${transpiledCode}
|
|
226
260
|
|
|
227
261
|
void setup() {
|
|
262
|
+
Serial.begin(115200);
|
|
263
|
+
nodalisLogInfo("Setup starting");
|
|
264
|
+
if (NODALIS_HEARTBEAT_LED_PIN >= 0) {
|
|
265
|
+
pinMode(NODALIS_HEARTBEAT_LED_PIN, OUTPUT);
|
|
266
|
+
digitalWrite(NODALIS_HEARTBEAT_LED_PIN, LOW);
|
|
267
|
+
}
|
|
268
|
+
localIp = nodalisLoadIpAddress();
|
|
269
|
+
nodalisBeginEthernet(localIp);
|
|
270
|
+
nodalisLogInfo("Ethernet initialized");
|
|
228
271
|
${globals.join('\n')}
|
|
229
|
-
modbusServer.start()
|
|
272
|
+
if (!modbusServer.start()) {
|
|
273
|
+
nodalisLogError("Modbus server start failed");
|
|
274
|
+
}
|
|
230
275
|
${mapCode}
|
|
276
|
+
nodalisLogInfo("Setup complete");
|
|
231
277
|
}
|
|
232
278
|
|
|
233
279
|
void loop() {
|
|
280
|
+
nodalisPollSerialIpConfig(localIp);
|
|
234
281
|
modbusServer.poll();
|
|
235
282
|
superviseIO();
|
|
283
|
+
nodalisHeartbeatTask();
|
|
236
284
|
${taskCode}
|
|
237
285
|
delay(1);
|
|
238
286
|
PROGRAM_COUNT++;
|
|
@@ -243,7 +291,7 @@ void loop() {
|
|
|
243
291
|
|
|
244
292
|
fs.mkdirSync(outputPath, { recursive: true });
|
|
245
293
|
fs.writeFileSync(inoFile, inoCode);
|
|
246
|
-
if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml')
|
|
294
|
+
if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml')) {
|
|
247
295
|
fs.writeFileSync(stFile, sourceCode);
|
|
248
296
|
}
|
|
249
297
|
|
|
@@ -254,6 +302,8 @@ void loop() {
|
|
|
254
302
|
fs.cpSync(path.join(supportDir, 'modbus.cpp'), path.join(outputPath, 'modbus.cpp'), { force: true });
|
|
255
303
|
fs.cpSync(path.join(supportDir, 'gpio.h'), path.join(outputPath, 'gpio.h'), { force: true });
|
|
256
304
|
fs.cpSync(path.join(supportDir, 'gpio.cpp'), path.join(outputPath, 'gpio.cpp'), { force: true });
|
|
305
|
+
fs.cpSync(path.join(supportDir, 'network_config.h'), path.join(outputPath, 'network_config.h'), { force: true });
|
|
306
|
+
fs.cpSync(path.join(supportDir, 'network_config.cpp'), path.join(outputPath, 'network_config.cpp'), { force: true });
|
|
257
307
|
fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
|
|
258
308
|
|
|
259
309
|
if (this.isExecutableOutput()) {
|
|
@@ -289,50 +339,6 @@ void loop() {
|
|
|
289
339
|
}
|
|
290
340
|
}
|
|
291
341
|
|
|
292
|
-
loadStructuredTextBundle(sourcePath, resourceName) {
|
|
293
|
-
const stFiles = this.listStructuredTextBundleFiles(sourcePath);
|
|
294
|
-
|
|
295
|
-
if (stFiles.length === 0) {
|
|
296
|
-
throw new Error(`No .st files found in source directory "${sourcePath}".`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const normalizedResource = String(resourceName || '').trim();
|
|
300
|
-
const candidateNames = new Set([
|
|
301
|
-
normalizedResource,
|
|
302
|
-
normalizedResource.toLowerCase(),
|
|
303
|
-
normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
|
|
304
|
-
]);
|
|
305
|
-
|
|
306
|
-
const entryFile = stFiles.find((file) => {
|
|
307
|
-
const lower = file.toLowerCase();
|
|
308
|
-
return candidateNames.has(file) || candidateNames.has(lower);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
if (!entryFile) {
|
|
312
|
-
throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const orderedFiles = [
|
|
316
|
-
...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
|
|
317
|
-
entryFile
|
|
318
|
-
];
|
|
319
|
-
|
|
320
|
-
const combinedSource = orderedFiles
|
|
321
|
-
.map((file) => fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim())
|
|
322
|
-
.filter((text) => text.length > 0)
|
|
323
|
-
.join('\n\n');
|
|
324
|
-
|
|
325
|
-
const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
|
|
326
|
-
const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
|
|
327
|
-
|
|
328
|
-
return { combinedSource, entryProgramName };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
extractFirstProgramName(sourceCode) {
|
|
332
|
-
const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
|
|
333
|
-
return match ? match[1] : null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
342
|
resolveArduinoFqbn(target, compilerConfig = {}) {
|
|
337
343
|
const explicit = compilerConfig.arduino_fqbn || compilerConfig.arduinoFqbn;
|
|
338
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
|
|
138
|
-
const transpiledCode = transpile
|
|
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")
|
|
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
|
|
106
|
-
const transpiledCode = transpile
|
|
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")
|
|
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 || []);
|
|
@@ -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 === '(') {
|