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 +7 -0
- package/package.json +1 -1
- package/src/compilers/ArduinoCompiler.js +10 -48
- 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 +38 -0
- package/src/compilers/st-parser/jstranspiler.js +29 -0
- package/src/compilers/st-parser/parser.js +5 -0
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
|
@@ -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 = [];
|
|
@@ -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')
|
|
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
|
|
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 || []);
|
|
@@ -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 === '(') {
|