nodalis-compiler 1.0.30 → 1.0.31
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
package/package.json
CHANGED
|
@@ -45,6 +45,163 @@ export class CPPCompiler extends Compiler {
|
|
|
45
45
|
this.name = 'CPPCompiler';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
loadStructuredTextBundle(sourcePath, resourceName) {
|
|
49
|
+
const exports = this.loadStructuredTextBundleExports(sourcePath, resourceName);
|
|
50
|
+
const stFiles = this.listStructuredTextBundleFiles(sourcePath);
|
|
51
|
+
|
|
52
|
+
if (stFiles.length === 0) {
|
|
53
|
+
throw new Error(`No .st files found in source directory "${sourcePath}".`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalizedResource = String(resourceName || '').trim();
|
|
57
|
+
const candidateNames = new Set([
|
|
58
|
+
normalizedResource,
|
|
59
|
+
normalizedResource.toLowerCase(),
|
|
60
|
+
normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const entryFile = stFiles.find((file) => {
|
|
64
|
+
const lower = file.toLowerCase();
|
|
65
|
+
return candidateNames.has(file) || candidateNames.has(lower);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!entryFile) {
|
|
69
|
+
throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const orderedFiles = [
|
|
73
|
+
...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
|
|
74
|
+
entryFile
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
let combinedSource = this.buildStructuredTextExportGlobals(exports);
|
|
78
|
+
let currentLine = combinedSource.length > 0 ? combinedSource.split('\n').length + 2 : 1;
|
|
79
|
+
const fileLineMappings = [];
|
|
80
|
+
|
|
81
|
+
for (const file of orderedFiles) {
|
|
82
|
+
const originalSource = fs.readFileSync(path.join(sourcePath, file), 'utf-8');
|
|
83
|
+
const fileSource = (file === entryFile
|
|
84
|
+
? this.addStructuredTextExportAssignments(originalSource, exports)
|
|
85
|
+
: originalSource).trim();
|
|
86
|
+
if (fileSource.length === 0) continue;
|
|
87
|
+
|
|
88
|
+
if (combinedSource.length > 0) {
|
|
89
|
+
combinedSource += '\n\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const lineCount = fileSource.split('\n').length;
|
|
93
|
+
fileLineMappings.push({
|
|
94
|
+
file,
|
|
95
|
+
startLine: currentLine,
|
|
96
|
+
endLine: currentLine + lineCount - 1
|
|
97
|
+
});
|
|
98
|
+
combinedSource += fileSource;
|
|
99
|
+
currentLine += lineCount + 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
|
|
103
|
+
const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
|
|
104
|
+
|
|
105
|
+
return { combinedSource, entryProgramName, fileLineMappings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
loadStructuredTextBundleExports(sourcePath, resourceName) {
|
|
109
|
+
const exportsPath = path.join(sourcePath, 'exports.json');
|
|
110
|
+
const resourceKey = this.getStructuredTextExportResourceKey(resourceName);
|
|
111
|
+
const defaultExports = {
|
|
112
|
+
T_MAIN: [{ global: 'Result', type: 'BOOL', address: '%MX0.0', variable: 'bResult' }]
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(exportsPath)) {
|
|
116
|
+
fs.writeFileSync(exportsPath, JSON.stringify(defaultExports, null, 4));
|
|
117
|
+
return resourceKey === this.getStructuredTextExportResourceKey('T_MAIN') ? defaultExports.T_MAIN : [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let exportConfig;
|
|
121
|
+
try {
|
|
122
|
+
exportConfig = JSON.parse(fs.readFileSync(exportsPath, 'utf-8'));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new Error(`Failed to load exports.json from ${exportsPath}: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof exportConfig !== 'object' || exportConfig === null || Array.isArray(exportConfig)) {
|
|
128
|
+
throw new Error(`exports.json in ${sourcePath} must be an object keyed by resource name.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const matchedKey = Object.keys(exportConfig).find((key) => this.getStructuredTextExportResourceKey(key) === resourceKey);
|
|
132
|
+
if (!matchedKey) return [];
|
|
133
|
+
|
|
134
|
+
const exports = exportConfig[matchedKey];
|
|
135
|
+
if (!Array.isArray(exports)) {
|
|
136
|
+
throw new Error(`exports.json entry for resource "${matchedKey}" must be an array.`);
|
|
137
|
+
}
|
|
138
|
+
return exports.map((entry, index) => this.validateStructuredTextBundleExport(entry, index, exportsPath));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getStructuredTextExportResourceKey(resourceName) {
|
|
142
|
+
return this.getStructuredTextExportResourceName(resourceName).toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getStructuredTextExportResourceName(resourceName) {
|
|
146
|
+
const normalized = String(resourceName || '').trim();
|
|
147
|
+
return path.basename(normalized, path.extname(normalized));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
validateStructuredTextBundleExport(entry, index, exportsPath) {
|
|
151
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
152
|
+
throw new Error(`Invalid export at ${exportsPath}[${index}]: expected an object.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const global = String(entry.global || '').trim();
|
|
156
|
+
const type = String(entry.type || 'BOOL').trim();
|
|
157
|
+
const address = String(entry.address || '').trim();
|
|
158
|
+
const variable = String(entry.variable || '').trim();
|
|
159
|
+
const identifierPattern = /^[A-Za-z_]\w*$/;
|
|
160
|
+
const variablePattern = /^[A-Za-z_]\w*(?:\.\w+)?$/;
|
|
161
|
+
const addressPattern = /^%[IQM][A-Z]*\d+(?:\.\d+)?$/i;
|
|
162
|
+
|
|
163
|
+
if (!identifierPattern.test(global)) {
|
|
164
|
+
throw new Error(`Invalid export global at ${exportsPath}[${index}]: "${entry.global}" is not a valid ST identifier.`);
|
|
165
|
+
}
|
|
166
|
+
if (!identifierPattern.test(type)) {
|
|
167
|
+
throw new Error(`Invalid export type at ${exportsPath}[${index}]: "${entry.type}" is not a valid ST type identifier.`);
|
|
168
|
+
}
|
|
169
|
+
if (!addressPattern.test(address)) {
|
|
170
|
+
throw new Error(`Invalid export address at ${exportsPath}[${index}]: "${entry.address}" is not a valid ST address.`);
|
|
171
|
+
}
|
|
172
|
+
if (!variablePattern.test(variable)) {
|
|
173
|
+
throw new Error(`Invalid export variable at ${exportsPath}[${index}]: "${entry.variable}" is not a valid ST variable reference.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { global, type, address, variable };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
buildStructuredTextExportGlobals(exports) {
|
|
180
|
+
if (!Array.isArray(exports) || exports.length === 0) return '';
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
'VAR_GLOBAL',
|
|
184
|
+
...exports.flatMap((entry) => [
|
|
185
|
+
` ${entry.global} AT ${entry.address} : ${entry.type};`,
|
|
186
|
+
`//Global=${JSON.stringify({ Name: entry.global, Address: entry.address })}`
|
|
187
|
+
]),
|
|
188
|
+
'END_VAR'
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
addStructuredTextExportAssignments(entrySource, exports) {
|
|
193
|
+
if (!Array.isArray(exports) || exports.length === 0) return entrySource;
|
|
194
|
+
|
|
195
|
+
const assignmentBlock = exports.map((entry) => `${entry.global} := ${entry.variable};`).join('\n');
|
|
196
|
+
const endProgramPattern = /^(\s*END_PROGRAM\b[^\r\n]*)/im;
|
|
197
|
+
|
|
198
|
+
if (!endProgramPattern.test(entrySource)) {
|
|
199
|
+
throw new Error('Entry program does not contain END_PROGRAM; cannot add exports.json assignments.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return entrySource.replace(endProgramPattern, `${assignmentBlock}\n$1`);
|
|
203
|
+
}
|
|
204
|
+
|
|
48
205
|
get supportedLanguages() {
|
|
49
206
|
return [IECLanguage.STRUCTURED_TEXT, IECLanguage.LADDER_DIAGRAM];
|
|
50
207
|
}
|
|
@@ -301,18 +458,22 @@ int main() {
|
|
|
301
458
|
|
|
302
459
|
inputs.push(`"${open62541o}"`);
|
|
303
460
|
inputs.push(`"${bacneta}"`);
|
|
461
|
+
const debugFlags = this.getDebugCompileFlags(compiler);
|
|
304
462
|
|
|
305
463
|
if (compiler === 'cl.exe') {
|
|
306
464
|
const cppFlagSegment = formatFlags(archFlags.cpp);
|
|
307
|
-
cppCompileCmd = `cl.exe /I${bacneti} /I${bacneti}/ports/${isWindowsTarget ? "win32" : "linux"} ${cppFlagSegment}/EHsc /std:c++17 /Fe:"${exeFile}" ` +
|
|
465
|
+
cppCompileCmd = `cl.exe /I${bacneti} /I${bacneti}/ports/${isWindowsTarget ? "win32" : "linux"} ${cppFlagSegment}${debugFlags} /EHsc /std:c++17 /Fe:"${exeFile}" ` +
|
|
308
466
|
`"${cppFile}" "${pathTo('nodalis.cpp')}" "${pathTo('modbus.cpp')}" "${pathTo('opcua.cpp')}" "${pathTo('bacnet.cpp')}" "${pathTo('gpio.cpp')}"`; //"${pathTo('open62541.obj')}"`;
|
|
309
467
|
} else {
|
|
310
468
|
const cppFlagSegment = formatFlags(archFlags.cpp);
|
|
311
|
-
cppCompileCmd = `${compiler} ${cppFlagSegment}-std=c++17 -I${bacneti} -I${bacneti}/ports/${isWindowsTarget ? "win32" : "linux"} -o "${exeFile}" ${inputs.join(' ')} ${archFlags.linker}`;
|
|
469
|
+
cppCompileCmd = `${compiler} ${cppFlagSegment}${debugFlags} -std=c++17 -I${bacneti} -I${bacneti}/ports/${isWindowsTarget ? "win32" : "linux"} -o "${exeFile}" ${inputs.join(' ')} ${archFlags.linker}`;
|
|
312
470
|
}
|
|
313
471
|
|
|
314
472
|
try {
|
|
315
473
|
execSync(cppCompileCmd, { stdio: 'pipe' });
|
|
474
|
+
if (targetInfo.os === 'macos') {
|
|
475
|
+
this.generateMacOSDebugSymbols(exeFile);
|
|
476
|
+
}
|
|
316
477
|
if (isWindowsTarget) {
|
|
317
478
|
this.copyWindowsRuntimeDependencies(compiler, binDir);
|
|
318
479
|
}
|
|
@@ -326,6 +487,19 @@ int main() {
|
|
|
326
487
|
}
|
|
327
488
|
}
|
|
328
489
|
|
|
490
|
+
getDebugCompileFlags(compiler) {
|
|
491
|
+
if (compiler === 'cl.exe') return '/Zi /Od';
|
|
492
|
+
return '-g -O0';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
generateMacOSDebugSymbols(exeFile) {
|
|
496
|
+
try {
|
|
497
|
+
execSync(`dsymutil "${exeFile}"`, { stdio: 'pipe' });
|
|
498
|
+
} catch {
|
|
499
|
+
// Debug symbols are best-effort; the executable is still usable without a dSYM.
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
329
503
|
copyWindowsRuntimeDependencies(compiler, binDir) {
|
|
330
504
|
const compilerName = String(compiler || '').toLowerCase();
|
|
331
505
|
const isMingwToolchain = compilerName.includes('mingw') || compilerName.includes('w64');
|
|
@@ -102,6 +102,7 @@ export function convertExpression(expr, isjsfb = false, jsfbVars = [], isjs=fals
|
|
|
102
102
|
.replace(/\b(?<![><!])=(?!=)/g, '=='); // ✅ fix assignment/comparison
|
|
103
103
|
|
|
104
104
|
results = rewriteExponentOperator(results, isjs);
|
|
105
|
+
results = rewriteNumericNotOperator(results);
|
|
105
106
|
|
|
106
107
|
// JS accepts 0o..., but C++ requires legacy octal form 0...
|
|
107
108
|
if (!isjs) {
|
|
@@ -150,6 +151,13 @@ export function convertExpression(expr, isjsfb = false, jsfbVars = [], isjs=fals
|
|
|
150
151
|
return results;
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
function rewriteNumericNotOperator(expression) {
|
|
155
|
+
return String(expression || '').replace(
|
|
156
|
+
/!\s+([+-]?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|\d+))/g,
|
|
157
|
+
'~$1'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
function rewriteExponentOperator(expression, isjs) {
|
|
154
162
|
let result = String(expression || '');
|
|
155
163
|
const operand = String.raw`(?:\([^()]*\)|[A-Za-z_]\w*(?:\.\d+)?|[+-]?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?))`;
|