nodalis-compiler 1.0.23 → 1.0.25
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 +4 -2
- package/src/compilers/CPPCompiler.js +6 -4
- package/src/compilers/JSCompiler.js +4 -2
- package/src/compilers/st-parser/gcctranspiler.js +18 -3
- package/src/compilers/st-parser/jstranspiler.js +18 -3
- package/src/compilers/st-parser/parser.js +170 -30
- package/src/compilers/st-parser/tokenizer.js +43 -14
- package/src/compilers/support/generic/bacnet.cpp +2 -4
- package/src/compilers/support/jint/nodalis/NodalisEngine/BacnetClient.cs +3 -2
- package/src/compilers/support/nodejs/bacnet.js +1 -2
- package/src/nodalis.js +2 -1
- package/src/programmers/ArduinoProgrammer.js +1 -0
- package/src/programmers/FileProgrammer.js +1 -0
- package/src/programmers/MTIProgrammer.js +1 -0
- package/src/programmers/SSHProgrammer.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.25] 2026-03-18
|
|
4
|
+
- Added line number errors to compilers.
|
|
5
|
+
- Changed bacnet to use remote address property in mapping as the instance number.
|
|
6
|
+
|
|
7
|
+
## [1.0.24] 2026-03-04
|
|
8
|
+
- Added support for CONFIGURATION, RESOURCE, TASK, and PROGRAM (instance) keywords in ST.
|
|
9
|
+
- Added "required" arrays to programmers for communicating required parameters beyond the base params when calling program.
|
|
10
|
+
|
|
3
11
|
## [1.0.23] 2026-03-03
|
|
4
12
|
- Changed IEC parser to interpret Function Blocks that are actually standard functions to a formal function call.
|
|
5
13
|
- Fixed nodejs/jint compiles to put executables in a bin folder.
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import { dirname } from 'node:path';
|
|
22
22
|
import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
|
|
23
23
|
import * as iec from './iec-parser/parser.js';
|
|
24
|
-
import { parseStructuredText } from './st-parser/parser.js';
|
|
24
|
+
import { parseStructuredText, buildCompilerMetadataDirectives } from './st-parser/parser.js';
|
|
25
25
|
import { transpile } from './st-parser/gcctranspiler.js';
|
|
26
26
|
import { DEFAULT_ARDUINO_FQBN } from './arduinoDefaults.js';
|
|
27
27
|
import { getManagedArduinoCliPath, getManagedArduinoCliExecOptions } from '../toolchains.js';
|
|
@@ -161,7 +161,9 @@ export class ArduinoCompiler extends Compiler {
|
|
|
161
161
|
let taskCode = '';
|
|
162
162
|
let mapCode = '';
|
|
163
163
|
|
|
164
|
-
const
|
|
164
|
+
const metadataDirectives = buildCompilerMetadataDirectives(parsed);
|
|
165
|
+
const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
|
|
166
|
+
const lines = metadataAwareSource.split('\n');
|
|
165
167
|
lines.forEach((line) => {
|
|
166
168
|
if (line.trim().startsWith('//Task=')) {
|
|
167
169
|
const task = JSON.parse(line.substring(line.indexOf('=') + 1).trim());
|
|
@@ -20,7 +20,7 @@ import fs from 'fs';
|
|
|
20
20
|
import path from "path";
|
|
21
21
|
import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
|
|
22
22
|
import * as iec from "./iec-parser/parser.js";
|
|
23
|
-
import { parseStructuredText } from './st-parser/parser.js';
|
|
23
|
+
import { parseStructuredText, buildCompilerMetadataDirectives } from './st-parser/parser.js';
|
|
24
24
|
import { transpile } from './st-parser/gcctranspiler.js';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { dirname } from 'node:path';
|
|
@@ -146,7 +146,9 @@ export class CPPCompiler extends Compiler {
|
|
|
146
146
|
if(typeof resourceName !== "undefined" && resourceName !== null){
|
|
147
147
|
plcname = resourceName;
|
|
148
148
|
}
|
|
149
|
-
const
|
|
149
|
+
const metadataDirectives = buildCompilerMetadataDirectives(parsed);
|
|
150
|
+
const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
|
|
151
|
+
const lines = metadataAwareSource.split("\n");
|
|
150
152
|
lines.forEach((line) => {
|
|
151
153
|
if(line.trim().startsWith("//Task=")){
|
|
152
154
|
var task = JSON.parse(line.substring(line.indexOf("=") + 1).trim());
|
|
@@ -557,8 +559,8 @@ int main() {
|
|
|
557
559
|
"windows-x64": " -lws2_32 -lcrypt32 -lwsock32 -lole32 -liphlpapi",
|
|
558
560
|
"windows-arm64": " -lws2_32 -lcrypt32 -lwsock32 -lole32 -liphlpapi",
|
|
559
561
|
"linux-x64": "",
|
|
560
|
-
"linux-arm64": "",
|
|
561
|
-
"linux-arm": "",
|
|
562
|
+
"linux-arm64": " -latomic",
|
|
563
|
+
"linux-arm": " -latomic",
|
|
562
564
|
"macos-x64": "",
|
|
563
565
|
"macos-arm64": "",
|
|
564
566
|
}
|
|
@@ -19,7 +19,7 @@ import os from "os";
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
|
|
21
21
|
import * as iec from "./iec-parser/parser.js";
|
|
22
|
-
import { parseStructuredText } from './st-parser/parser.js';
|
|
22
|
+
import { parseStructuredText, buildCompilerMetadataDirectives } from './st-parser/parser.js';
|
|
23
23
|
import { transpile } from './st-parser/jstranspiler.js';
|
|
24
24
|
import which from "which";
|
|
25
25
|
import { fileURLToPath } from "url";
|
|
@@ -114,7 +114,9 @@ export class JSCompiler extends Compiler {
|
|
|
114
114
|
if(typeof resourceName !== "undefined" && resourceName !== null){
|
|
115
115
|
plcname = resourceName;
|
|
116
116
|
}
|
|
117
|
-
const
|
|
117
|
+
const metadataDirectives = buildCompilerMetadataDirectives(parsed);
|
|
118
|
+
const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
|
|
119
|
+
const lines = metadataAwareSource.split("\n");
|
|
118
120
|
lines.forEach((line) => {
|
|
119
121
|
if(line.trim().startsWith("//Task=")){
|
|
120
122
|
var task = JSON.parse(line.substring(line.indexOf("=") + 1).trim());
|
|
@@ -179,13 +179,12 @@ function mapStatement(stmt){
|
|
|
179
179
|
return [`${stmt.name}();`];
|
|
180
180
|
}
|
|
181
181
|
default:
|
|
182
|
-
|
|
182
|
+
throw createTranspileError(`Unsupported statement type '${stmt.type}'`, stmt);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
catch(e){
|
|
186
|
-
|
|
186
|
+
throw enrichTranspileError(e, stmt);
|
|
187
187
|
}
|
|
188
|
-
return "// uncompilable statement " + JSON.stringify(stmt);
|
|
189
188
|
}
|
|
190
189
|
|
|
191
190
|
/**
|
|
@@ -197,6 +196,22 @@ function transpileStatements(statements) {
|
|
|
197
196
|
return statements?.flatMap(mapStatement);
|
|
198
197
|
}
|
|
199
198
|
|
|
199
|
+
function createTranspileError(message, node) {
|
|
200
|
+
const line = node?.loc?.line ?? 1;
|
|
201
|
+
const column = node?.loc?.column ?? 1;
|
|
202
|
+
const error = new Error(`${message} at line ${line}, column ${column}`);
|
|
203
|
+
error.line = line;
|
|
204
|
+
error.column = column;
|
|
205
|
+
error.sourceLocation = { line, column };
|
|
206
|
+
return error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function enrichTranspileError(error, node) {
|
|
210
|
+
if (error?.line) return error;
|
|
211
|
+
const message = error?.message || String(error);
|
|
212
|
+
return createTranspileError(message, node);
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
/**
|
|
201
216
|
* Creates a transpiled section of declared variables.
|
|
202
217
|
* @param {{type: string, address: string, initialValue: string, sectionType: string}[]} varSections An array of variable tokens.
|
|
@@ -196,11 +196,10 @@ function mapStatement(stmt, infb = false) {
|
|
|
196
196
|
return [`${stmt.name}();`];
|
|
197
197
|
}
|
|
198
198
|
default:
|
|
199
|
-
|
|
199
|
+
throw createTranspileError(`Unsupported statement type '${stmt.type}'`, stmt);
|
|
200
200
|
}
|
|
201
201
|
} catch (e) {
|
|
202
|
-
|
|
203
|
-
return [`// Failed to transpile: ${JSON.stringify(stmt)}`];
|
|
202
|
+
throw enrichTranspileError(e, stmt);
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
|
|
@@ -213,6 +212,22 @@ function transpileStatements(statements, infb=false) {
|
|
|
213
212
|
return statements?.flatMap((stmt) => mapStatement(stmt, infb));
|
|
214
213
|
}
|
|
215
214
|
|
|
215
|
+
function createTranspileError(message, node) {
|
|
216
|
+
const line = node?.loc?.line ?? 1;
|
|
217
|
+
const column = node?.loc?.column ?? 1;
|
|
218
|
+
const error = new Error(`${message} at line ${line}, column ${column}`);
|
|
219
|
+
error.line = line;
|
|
220
|
+
error.column = column;
|
|
221
|
+
error.sourceLocation = { line, column };
|
|
222
|
+
return error;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function enrichTranspileError(error, node) {
|
|
226
|
+
if (error?.line) return error;
|
|
227
|
+
const message = error?.message || String(error);
|
|
228
|
+
return createTranspileError(message, node);
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
/**
|
|
217
232
|
* Creates a transpiled section of declared variables.
|
|
218
233
|
* @param {{type: string, address: string, initialValue: string, sectionType: string}[]} varSections An array of variable tokens.
|
|
@@ -32,19 +32,47 @@ import { tokenize } from './tokenizer.js';
|
|
|
32
32
|
export function parseStructuredText(code) {
|
|
33
33
|
const tokens = tokenize(code);
|
|
34
34
|
let position = 0;
|
|
35
|
+
const TOP_LEVEL_STARTERS = new Set([
|
|
36
|
+
'PROGRAM',
|
|
37
|
+
'FUNCTION',
|
|
38
|
+
'FUNCTION_BLOCK',
|
|
39
|
+
'VAR_GLOBAL',
|
|
40
|
+
'CONFIGURATION',
|
|
41
|
+
'RESOURCE',
|
|
42
|
+
'TASK'
|
|
43
|
+
]);
|
|
35
44
|
|
|
36
45
|
function peek(offset = 0) {
|
|
37
46
|
return tokens[position + offset];
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
function previous() {
|
|
50
|
+
return tokens[position - 1];
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function consume() {
|
|
41
54
|
return tokens[position++];
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
function withLoc(node, token) {
|
|
58
|
+
if (!node || !token) return node;
|
|
59
|
+
return { ...node, loc: { line: token.line, column: token.column } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createSourceError(message, token = peek() || previous()) {
|
|
63
|
+
const line = token?.line ?? 1;
|
|
64
|
+
const column = token?.column ?? 1;
|
|
65
|
+
const error = new Error(`${message} at line ${line}, column ${column}`);
|
|
66
|
+
error.line = line;
|
|
67
|
+
error.column = column;
|
|
68
|
+
error.sourceLocation = { line, column };
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
function expect(value) {
|
|
45
73
|
const token = consume();
|
|
46
74
|
if (!token || token.value.toUpperCase() !== value.toUpperCase()) {
|
|
47
|
-
throw
|
|
75
|
+
throw createSourceError(`Expected '${value}', but got '${token?.value ?? 'EOF'}'`, token || previous());
|
|
48
76
|
}
|
|
49
77
|
return token;
|
|
50
78
|
}
|
|
@@ -55,19 +83,101 @@ export function parseStructuredText(code) {
|
|
|
55
83
|
|
|
56
84
|
switch (token.value.toUpperCase()) {
|
|
57
85
|
case 'PROGRAM':
|
|
58
|
-
return
|
|
86
|
+
return parseProgramOrInstance();
|
|
59
87
|
case 'FUNCTION':
|
|
60
88
|
return parseFunction();
|
|
61
89
|
case 'FUNCTION_BLOCK':
|
|
62
90
|
return parseFunctionBlock();
|
|
63
91
|
case 'VAR_GLOBAL':
|
|
64
92
|
return parseGlobalVarSection();
|
|
93
|
+
case 'TASK':
|
|
94
|
+
return parseTask();
|
|
95
|
+
case 'CONFIGURATION':
|
|
96
|
+
return parseContainer('CONFIGURATION', 'END_CONFIGURATION');
|
|
97
|
+
case 'RESOURCE':
|
|
98
|
+
return parseContainer('RESOURCE', 'END_RESOURCE');
|
|
65
99
|
default:
|
|
66
100
|
consume();
|
|
67
101
|
return null;
|
|
68
102
|
}
|
|
69
103
|
}
|
|
70
104
|
|
|
105
|
+
function parseContainer(startKeyword, endKeyword) {
|
|
106
|
+
expect(startKeyword);
|
|
107
|
+
const blocks = [];
|
|
108
|
+
|
|
109
|
+
while (peek() && peek().value.toUpperCase() !== endKeyword) {
|
|
110
|
+
const upper = peek().value.toUpperCase();
|
|
111
|
+
if (TOP_LEVEL_STARTERS.has(upper)) {
|
|
112
|
+
const block = parseBlock();
|
|
113
|
+
if (Array.isArray(block)) blocks.push(...block);
|
|
114
|
+
else if (block) blocks.push(block);
|
|
115
|
+
} else {
|
|
116
|
+
consume();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expect(endKeyword);
|
|
121
|
+
return blocks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseTask() {
|
|
125
|
+
expect('TASK');
|
|
126
|
+
const name = consume().value;
|
|
127
|
+
let interval = '1000';
|
|
128
|
+
let priority = '1';
|
|
129
|
+
|
|
130
|
+
if (peek()?.value === '(') {
|
|
131
|
+
consume();
|
|
132
|
+
while (peek() && peek().value !== ')') {
|
|
133
|
+
const key = consume().value.toUpperCase();
|
|
134
|
+
if (peek()?.value === ':=') consume();
|
|
135
|
+
|
|
136
|
+
const valueTokens = [];
|
|
137
|
+
while (peek() && peek().value !== ',' && peek().value !== ')') {
|
|
138
|
+
valueTokens.push(consume().value);
|
|
139
|
+
}
|
|
140
|
+
const value = valueTokens.join('');
|
|
141
|
+
|
|
142
|
+
if (key === 'INTERVAL') interval = normalizeTaskInterval(value);
|
|
143
|
+
else if (key === 'PRIORITY') priority = normalizeNumericString(value, '1');
|
|
144
|
+
|
|
145
|
+
if (peek()?.value === ',') consume();
|
|
146
|
+
}
|
|
147
|
+
expect(')');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (peek()?.value === ';') consume();
|
|
151
|
+
return withLoc({ type: 'TaskDeclaration', name, interval, priority }, tokens[position - 1]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeNumericString(value, fallback) {
|
|
155
|
+
const match = String(value || '').match(/-?\d+/);
|
|
156
|
+
return match ? match[0] : fallback;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeTaskInterval(value) {
|
|
160
|
+
const text = String(value || '').trim();
|
|
161
|
+
if (!text) return '1000';
|
|
162
|
+
|
|
163
|
+
if (/^-?\d+$/.test(text)) return text;
|
|
164
|
+
|
|
165
|
+
const duration = text.match(/^T#?([+-]?\d+(?:\.\d+)?)(MS|S|M|H|D)?$/i);
|
|
166
|
+
if (!duration) return normalizeNumericString(text, '1000');
|
|
167
|
+
|
|
168
|
+
const amount = Number(duration[1]);
|
|
169
|
+
const unit = (duration[2] || 'MS').toUpperCase();
|
|
170
|
+
const multipliers = {
|
|
171
|
+
MS: 1,
|
|
172
|
+
S: 1000,
|
|
173
|
+
M: 60000,
|
|
174
|
+
H: 3600000,
|
|
175
|
+
D: 86400000
|
|
176
|
+
};
|
|
177
|
+
const ms = Math.round(amount * (multipliers[unit] || 1));
|
|
178
|
+
return String(ms);
|
|
179
|
+
}
|
|
180
|
+
|
|
71
181
|
function parseGlobalVarSection() {
|
|
72
182
|
expect('VAR_GLOBAL');
|
|
73
183
|
const variables = [];
|
|
@@ -82,7 +192,7 @@ export function parseStructuredText(code) {
|
|
|
82
192
|
if (addrToken?.type === 'ADDRESS' || addrToken?.type === 'IDENTIFIER') {
|
|
83
193
|
address = addrToken.value;
|
|
84
194
|
} else {
|
|
85
|
-
throw
|
|
195
|
+
throw createSourceError(`Expected address after AT, got '${addrToken?.value ?? 'EOF'}'`, addrToken || previous());
|
|
86
196
|
}
|
|
87
197
|
}
|
|
88
198
|
expect(':');
|
|
@@ -97,12 +207,12 @@ export function parseStructuredText(code) {
|
|
|
97
207
|
}
|
|
98
208
|
initialValue = initTokens.join('');
|
|
99
209
|
}
|
|
100
|
-
variables.push({ name, type, address, initialValue, sectionType: 'VAR_GLOBAL' });
|
|
210
|
+
variables.push(withLoc({ name, type, address, initialValue, sectionType: 'VAR_GLOBAL' }, tokens[position - 1]));
|
|
101
211
|
if (peek()?.value === ';') consume();
|
|
102
212
|
}
|
|
103
213
|
|
|
104
214
|
expect('END_VAR');
|
|
105
|
-
return { type: 'GlobalVars', variables };
|
|
215
|
+
return withLoc({ type: 'GlobalVars', variables }, variables[0]?.loc ? { line: variables[0].loc.line, column: variables[0].loc.column } : previous());
|
|
106
216
|
}
|
|
107
217
|
|
|
108
218
|
|
|
@@ -111,6 +221,7 @@ export function parseStructuredText(code) {
|
|
|
111
221
|
const sectionType = consume().value.toUpperCase();
|
|
112
222
|
|
|
113
223
|
while (peek() && peek().value.toUpperCase() !== 'END_VAR') {
|
|
224
|
+
const startToken = peek();
|
|
114
225
|
const name = consume().value;
|
|
115
226
|
expect(':');
|
|
116
227
|
const type = consume().value;
|
|
@@ -124,7 +235,7 @@ export function parseStructuredText(code) {
|
|
|
124
235
|
}
|
|
125
236
|
initialValue = initTokens.join('');
|
|
126
237
|
}
|
|
127
|
-
variables.push({ name, type, initialValue, sectionType });
|
|
238
|
+
variables.push(withLoc({ name, type, initialValue, sectionType }, startToken));
|
|
128
239
|
if (peek()?.value === ';') consume();
|
|
129
240
|
}
|
|
130
241
|
expect('END_VAR');
|
|
@@ -143,6 +254,10 @@ export function parseStructuredText(code) {
|
|
|
143
254
|
function parseStatement() {
|
|
144
255
|
const token = peek();
|
|
145
256
|
if (!token) return null;
|
|
257
|
+
if (token.value === ';') {
|
|
258
|
+
consume();
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
146
261
|
|
|
147
262
|
if (token.value.toUpperCase() === 'IF') return parseIf();
|
|
148
263
|
if (token.value.toUpperCase() === 'WHILE') return parseWhile();
|
|
@@ -170,7 +285,7 @@ function parseStatement() {
|
|
|
170
285
|
// optional semicolon
|
|
171
286
|
if (peek()?.value === ';') consume();
|
|
172
287
|
|
|
173
|
-
return { type: 'CALL', name, args };
|
|
288
|
+
return withLoc({ type: 'CALL', name, args }, token);
|
|
174
289
|
}
|
|
175
290
|
|
|
176
291
|
// Assignment: x := y;
|
|
@@ -189,16 +304,15 @@ if (peek(i)?.value === ':=') {
|
|
|
189
304
|
right.push(consume().value);
|
|
190
305
|
}
|
|
191
306
|
if (peek()?.value === ';') consume();
|
|
192
|
-
return { type: 'ASSIGN', left: lhs, right };
|
|
307
|
+
return withLoc({ type: 'ASSIGN', left: lhs, right }, lhsTokens[0]);
|
|
193
308
|
}
|
|
194
309
|
|
|
195
|
-
|
|
196
|
-
return null;
|
|
310
|
+
throw createSourceError(`Unexpected token '${token.value}'`, token);
|
|
197
311
|
}
|
|
198
312
|
|
|
199
313
|
|
|
200
314
|
function parseIf() {
|
|
201
|
-
consume(); // IF
|
|
315
|
+
const startToken = consume(); // IF
|
|
202
316
|
|
|
203
317
|
// Collect condition tokens until THEN
|
|
204
318
|
const conditionTokens = [];
|
|
@@ -231,13 +345,13 @@ function parseIf() {
|
|
|
231
345
|
consume(); // END_IF
|
|
232
346
|
}
|
|
233
347
|
|
|
234
|
-
return {
|
|
348
|
+
return withLoc({
|
|
235
349
|
type: 'IF',
|
|
236
350
|
condition: conditionTokens,
|
|
237
351
|
thenBlock,
|
|
238
352
|
elseIfBlocks,
|
|
239
353
|
elseBlock
|
|
240
|
-
};
|
|
354
|
+
}, startToken);
|
|
241
355
|
}
|
|
242
356
|
|
|
243
357
|
|
|
@@ -258,7 +372,7 @@ function parseStatementsUntil(endTokens) {
|
|
|
258
372
|
|
|
259
373
|
|
|
260
374
|
function parseWhile() {
|
|
261
|
-
consume(); // WHILE
|
|
375
|
+
const startToken = consume(); // WHILE
|
|
262
376
|
const condition = [];
|
|
263
377
|
while (peek() && peek().value.toUpperCase() !== 'DO') {
|
|
264
378
|
condition.push(consume().value);
|
|
@@ -266,11 +380,11 @@ function parseStatementsUntil(endTokens) {
|
|
|
266
380
|
expect('DO');
|
|
267
381
|
const body = parseStatements('END_WHILE');
|
|
268
382
|
expect('END_WHILE');
|
|
269
|
-
return { type: 'WHILE', condition, body };
|
|
383
|
+
return withLoc({ type: 'WHILE', condition, body }, startToken);
|
|
270
384
|
}
|
|
271
385
|
|
|
272
386
|
function parseFor() {
|
|
273
|
-
consume(); // FOR
|
|
387
|
+
const startToken = consume(); // FOR
|
|
274
388
|
const variable = consume().value;
|
|
275
389
|
expect(':=');
|
|
276
390
|
const from = consume().value;
|
|
@@ -284,11 +398,11 @@ function parseStatementsUntil(endTokens) {
|
|
|
284
398
|
expect('DO');
|
|
285
399
|
const body = parseStatements('END_FOR');
|
|
286
400
|
expect('END_FOR');
|
|
287
|
-
return { type: 'FOR', variable, from, to, step, body };
|
|
401
|
+
return withLoc({ type: 'FOR', variable, from, to, step, body }, startToken);
|
|
288
402
|
}
|
|
289
403
|
|
|
290
404
|
function parseRepeat() {
|
|
291
|
-
consume(); // REPEAT
|
|
405
|
+
const startToken = consume(); // REPEAT
|
|
292
406
|
const body = parseStatements('UNTIL');
|
|
293
407
|
expect('UNTIL');
|
|
294
408
|
const condition = [];
|
|
@@ -297,11 +411,11 @@ function parseStatementsUntil(endTokens) {
|
|
|
297
411
|
}
|
|
298
412
|
expect('END_REPEAT');
|
|
299
413
|
if (peek()?.value === ';') consume();
|
|
300
|
-
return { type: 'REPEAT', condition, body };
|
|
414
|
+
return withLoc({ type: 'REPEAT', condition, body }, startToken);
|
|
301
415
|
}
|
|
302
416
|
|
|
303
417
|
function parseCase() {
|
|
304
|
-
consume(); // CASE
|
|
418
|
+
const startToken = consume(); // CASE
|
|
305
419
|
const expression = [];
|
|
306
420
|
while (peek() && peek().value.toUpperCase() !== 'OF') {
|
|
307
421
|
expression.push(consume().value);
|
|
@@ -320,7 +434,7 @@ function parseStatementsUntil(endTokens) {
|
|
|
320
434
|
const stmt = parseStatement();
|
|
321
435
|
if (stmt) body.push(stmt);
|
|
322
436
|
}
|
|
323
|
-
branches.push({ label: labels.join(','), body });
|
|
437
|
+
branches.push(withLoc({ label: labels.join(','), body }, peek(-1)));
|
|
324
438
|
}
|
|
325
439
|
let elseBlock = null;
|
|
326
440
|
if (peek()?.value.toUpperCase() === 'ELSE') {
|
|
@@ -332,7 +446,7 @@ function parseStatementsUntil(endTokens) {
|
|
|
332
446
|
}
|
|
333
447
|
}
|
|
334
448
|
expect('END_CASE');
|
|
335
|
-
return { type: 'CASE', expression, branches, elseBlock };
|
|
449
|
+
return withLoc({ type: 'CASE', expression, branches, elseBlock }, startToken);
|
|
336
450
|
}
|
|
337
451
|
|
|
338
452
|
function isCaseBranchBoundary(token) {
|
|
@@ -342,9 +456,22 @@ function parseStatementsUntil(endTokens) {
|
|
|
342
456
|
return peek(1)?.value === ':' || peek(1)?.value === ',';
|
|
343
457
|
}
|
|
344
458
|
|
|
345
|
-
function
|
|
346
|
-
expect('PROGRAM');
|
|
459
|
+
function parseProgramOrInstance() {
|
|
460
|
+
const startToken = expect('PROGRAM');
|
|
347
461
|
const name = consume().value;
|
|
462
|
+
|
|
463
|
+
if (peek()?.value?.toUpperCase() === 'WITH' || peek()?.value === ':') {
|
|
464
|
+
let associatedTaskName = '';
|
|
465
|
+
if (peek()?.value?.toUpperCase() === 'WITH') {
|
|
466
|
+
consume();
|
|
467
|
+
associatedTaskName = consume().value;
|
|
468
|
+
}
|
|
469
|
+
expect(':');
|
|
470
|
+
const typeName = consume().value;
|
|
471
|
+
if (peek()?.value === ';') consume();
|
|
472
|
+
return withLoc({ type: 'ProgramInstanceDeclaration', name, typeName, associatedTaskName }, startToken);
|
|
473
|
+
}
|
|
474
|
+
|
|
348
475
|
const vars = [];
|
|
349
476
|
const stmts = [];
|
|
350
477
|
|
|
@@ -355,11 +482,11 @@ function parseStatementsUntil(endTokens) {
|
|
|
355
482
|
stmts.push(...parseStatements('END_PROGRAM'));
|
|
356
483
|
expect('END_PROGRAM');
|
|
357
484
|
|
|
358
|
-
return { type: 'ProgramDeclaration', name, varSections: vars, statements: stmts };
|
|
485
|
+
return withLoc({ type: 'ProgramDeclaration', name, varSections: vars, statements: stmts }, startToken);
|
|
359
486
|
}
|
|
360
487
|
|
|
361
488
|
function parseFunction() {
|
|
362
|
-
expect('FUNCTION');
|
|
489
|
+
const startToken = expect('FUNCTION');
|
|
363
490
|
const name = consume().value;
|
|
364
491
|
expect(':');
|
|
365
492
|
const returnType = consume().value;
|
|
@@ -373,11 +500,11 @@ function parseStatementsUntil(endTokens) {
|
|
|
373
500
|
stmts.push(...parseStatements('END_FUNCTION'));
|
|
374
501
|
expect('END_FUNCTION');
|
|
375
502
|
|
|
376
|
-
return { type: 'FunctionDeclaration', name, returnType, varSections: vars, statements: stmts };
|
|
503
|
+
return withLoc({ type: 'FunctionDeclaration', name, returnType, varSections: vars, statements: stmts }, startToken);
|
|
377
504
|
}
|
|
378
505
|
|
|
379
506
|
function parseFunctionBlock() {
|
|
380
|
-
expect('FUNCTION_BLOCK');
|
|
507
|
+
const startToken = expect('FUNCTION_BLOCK');
|
|
381
508
|
const name = consume().value;
|
|
382
509
|
const vars = [];
|
|
383
510
|
const stmts = [];
|
|
@@ -389,14 +516,27 @@ function parseStatementsUntil(endTokens) {
|
|
|
389
516
|
stmts.push(...parseStatements('END_FUNCTION_BLOCK'));
|
|
390
517
|
expect('END_FUNCTION_BLOCK');
|
|
391
518
|
|
|
392
|
-
return { type: 'FunctionBlockDeclaration', name, varSections: vars, statements: stmts };
|
|
519
|
+
return withLoc({ type: 'FunctionBlockDeclaration', name, varSections: vars, statements: stmts }, startToken);
|
|
393
520
|
}
|
|
394
521
|
|
|
395
522
|
const body = [];
|
|
396
523
|
while (position < tokens.length) {
|
|
397
524
|
const block = parseBlock();
|
|
398
|
-
if (block) body.push(block);
|
|
525
|
+
if (Array.isArray(block)) body.push(...block);
|
|
526
|
+
else if (block) body.push(block);
|
|
399
527
|
}
|
|
400
528
|
|
|
401
529
|
return { type: 'Program', body };
|
|
402
530
|
}
|
|
531
|
+
|
|
532
|
+
export function buildCompilerMetadataDirectives(ast) {
|
|
533
|
+
const lines = [];
|
|
534
|
+
for (const block of ast?.body || []) {
|
|
535
|
+
if (block?.type === 'TaskDeclaration') {
|
|
536
|
+
lines.push(`//Task={"Name":"${block.name}", "Interval":"${block.interval}", "Priority":"${block.priority}"}`);
|
|
537
|
+
} else if (block?.type === 'ProgramInstanceDeclaration') {
|
|
538
|
+
lines.push(`//Instance={"TypeName":"${block.typeName}", "Name":"${block.name}", "AssociatedTaskName":"${block.associatedTaskName || ''}"}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return lines.join('\n') + (lines.length > 0 ? '\n' : '');
|
|
542
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
/**
|
|
25
25
|
* Tokenizes a block of structured text into their types and values.
|
|
26
26
|
* @param {string} code A block of structured text code.
|
|
27
|
-
* @returns {{type: string, value: string}[]} An array of tokens.
|
|
27
|
+
* @returns {{type: string, value: string, line: number, column: number}[]} An array of tokens.
|
|
28
28
|
*/
|
|
29
29
|
const INTEGER_TYPE_PATTERN = '(?:BYTE|WORD|DWORD|LWORD|SINT|INT|DINT|LINT|USINT|UINT|UDINT|ULINT)';
|
|
30
30
|
const REAL_TYPE_PATTERN = '(?:REAL|LREAL)';
|
|
@@ -40,37 +40,66 @@ const NUMBER_TOKEN_PATTERN = `(?:${TYPED_INTEGER_LITERAL_PATTERN}|${TYPED_REAL_L
|
|
|
40
40
|
export function tokenize(code) {
|
|
41
41
|
const tokens = [];
|
|
42
42
|
let match;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Remove multi-line comments ((*...*))
|
|
47
|
-
code = code.replace(/\(\*[\s\S]*?\*\)/g, '');
|
|
43
|
+
const sanitizedCode = stripCommentsPreservePositions(code);
|
|
44
|
+
const lineStarts = buildLineStarts(sanitizedCode);
|
|
48
45
|
|
|
49
46
|
//const regex = /(%[IQM][A-Z]?[0-9]+(?:\.[0-9]+)?)|(:=)|([A-Za-z_]\w*\.\d+)|([A-Za-z_]\w*\.\w+)|([A-Za-z_]\w*)|(\d+)|([:;()<>+\-*/=])/g;
|
|
50
47
|
const regex = new RegExp(`(%[IQM][A-Z]*\\d+(?:\\.\\d+)?)|(:=|=>|>=|<=|<>|!=|\\*\\*)|([A-Za-z_]\\w*\\.\\d+)|([A-Za-z_]\\w*\\.\\w+)|(${NUMBER_TOKEN_PATTERN})|([A-Za-z_]\\w*)|([<>+\\-*/=;():,&|^])`, 'gi');
|
|
51
48
|
|
|
52
|
-
while ((match = regex.exec(
|
|
49
|
+
while ((match = regex.exec(sanitizedCode)) !== null) {
|
|
53
50
|
const [_, address, compoundSymbol, bitIdentifier, propIdentifier, number, identifier, symbol] = match;
|
|
51
|
+
const location = getLineColumn(lineStarts, match.index);
|
|
54
52
|
|
|
55
53
|
if (address)
|
|
56
|
-
tokens.push({ type: 'ADDRESS', value: address });
|
|
54
|
+
tokens.push({ type: 'ADDRESS', value: address, ...location });
|
|
57
55
|
else if (compoundSymbol)
|
|
58
|
-
tokens.push({ type: 'SYMBOL', value: compoundSymbol });
|
|
56
|
+
tokens.push({ type: 'SYMBOL', value: compoundSymbol, ...location });
|
|
59
57
|
else if (bitIdentifier)
|
|
60
|
-
tokens.push({ type: 'IDENTIFIER', value: bitIdentifier });
|
|
58
|
+
tokens.push({ type: 'IDENTIFIER', value: bitIdentifier, ...location });
|
|
61
59
|
else if (propIdentifier)
|
|
62
|
-
tokens.push({ type: 'IDENTIFIER', value: propIdentifier });
|
|
60
|
+
tokens.push({ type: 'IDENTIFIER', value: propIdentifier, ...location });
|
|
63
61
|
else if (identifier)
|
|
64
|
-
tokens.push({ type: 'IDENTIFIER', value: identifier });
|
|
62
|
+
tokens.push({ type: 'IDENTIFIER', value: identifier, ...location });
|
|
65
63
|
else if (number)
|
|
66
|
-
tokens.push({ type: 'NUMBER', value: normalizeNumericLiteral(number) });
|
|
64
|
+
tokens.push({ type: 'NUMBER', value: normalizeNumericLiteral(number), ...location });
|
|
67
65
|
else if (symbol)
|
|
68
|
-
tokens.push({ type: 'SYMBOL', value: symbol });
|
|
66
|
+
tokens.push({ type: 'SYMBOL', value: symbol, ...location });
|
|
69
67
|
|
|
70
68
|
}
|
|
71
69
|
return tokens;
|
|
72
70
|
}
|
|
73
71
|
|
|
72
|
+
function stripCommentsPreservePositions(code) {
|
|
73
|
+
return String(code || '')
|
|
74
|
+
.replace(/\/\/.*$/gm, (comment) => ' '.repeat(comment.length))
|
|
75
|
+
.replace(/\(\*[\s\S]*?\*\)/g, (comment) => comment.replace(/[^\n]/g, ' '));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildLineStarts(text) {
|
|
79
|
+
const starts = [0];
|
|
80
|
+
for (let i = 0; i < text.length; i++) {
|
|
81
|
+
if (text[i] === '\n') starts.push(i + 1);
|
|
82
|
+
}
|
|
83
|
+
return starts;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getLineColumn(lineStarts, index) {
|
|
87
|
+
let low = 0;
|
|
88
|
+
let high = lineStarts.length - 1;
|
|
89
|
+
|
|
90
|
+
while (low <= high) {
|
|
91
|
+
const mid = Math.floor((low + high) / 2);
|
|
92
|
+
if (lineStarts[mid] <= index) low = mid + 1;
|
|
93
|
+
else high = mid - 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lineIndex = Math.max(0, high);
|
|
97
|
+
return {
|
|
98
|
+
line: lineIndex + 1,
|
|
99
|
+
column: (index - lineStarts[lineIndex]) + 1
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
74
103
|
function normalizeNumericLiteral(value) {
|
|
75
104
|
const literal = String(value);
|
|
76
105
|
const boolMatch = literal.match(new RegExp(`^${BOOL_TYPE_PATTERN}#(TRUE|FALSE|1|0)$`, 'i'));
|
|
@@ -428,6 +428,7 @@ bool BACNETClient::resolveRemote(const std::string& remote, BACnetRemotePoint& p
|
|
|
428
428
|
bool BACNETClient::parseRemoteDefinition(const IOMap &map, BACnetRemotePoint &point)
|
|
429
429
|
{
|
|
430
430
|
json config = nullptr;
|
|
431
|
+
point.objectInstance = static_cast<uint32_t>(std::stoll(map.remoteAddress));
|
|
431
432
|
if (map.additionalProperties.is_string())
|
|
432
433
|
{
|
|
433
434
|
std::cout << "BACNET-IP Additional Properties is a string.\n";
|
|
@@ -438,6 +439,7 @@ bool BACNETClient::parseRemoteDefinition(const IOMap &map, BACnetRemotePoint &po
|
|
|
438
439
|
config = map.additionalProperties;
|
|
439
440
|
std::cout << "BACNET-IP Additional Properties is an object.\n";
|
|
440
441
|
}
|
|
442
|
+
|
|
441
443
|
if (parseJsonRemote(config, point))
|
|
442
444
|
{
|
|
443
445
|
return true;
|
|
@@ -465,10 +467,6 @@ bool BACNETClient::parseJsonRemote(const json& config, BACnetRemotePoint& point)
|
|
|
465
467
|
extractNumber(config, "ObjectInstance", instance)) {
|
|
466
468
|
point.objectInstance = instance;
|
|
467
469
|
}
|
|
468
|
-
else
|
|
469
|
-
{
|
|
470
|
-
std::cout << "BACNET-IP no object instance\n";
|
|
471
|
-
}
|
|
472
470
|
|
|
473
471
|
auto propertyToken = extractString(config, "propertyId");
|
|
474
472
|
if (!propertyToken) {
|
|
@@ -226,12 +226,13 @@ namespace Nodalis
|
|
|
226
226
|
// Required numeric fields.
|
|
227
227
|
if (!TryGetUInt(props, "objectType", out var objType) && !TryGetUInt(props, "ObjectType", out objType))
|
|
228
228
|
return false;
|
|
229
|
-
if (!TryGetUInt(props, "objectInstance", out var objInst) && !TryGetUInt(props, "ObjectInstance", out objInst))
|
|
230
|
-
return false;
|
|
231
229
|
if (!TryGetUInt(props, "propertyId", out var propId) && !TryGetUInt(props, "PropertyId", out propId))
|
|
232
230
|
return false;
|
|
231
|
+
if (!uint.TryParse(remoteAddress, out var objInst))
|
|
232
|
+
return false;
|
|
233
233
|
|
|
234
234
|
point.ObjectType = (BacnetObjectTypes)objType;
|
|
235
|
+
|
|
235
236
|
point.ObjectInstance = objInst;
|
|
236
237
|
point.PropertyId = (BacnetPropertyIds)propId;
|
|
237
238
|
|
|
@@ -253,13 +253,12 @@ export class BacnetClient extends IOClient {
|
|
|
253
253
|
const stringRemote = parseRemoteString(map.remoteAddress);
|
|
254
254
|
|
|
255
255
|
const objectTypeRaw = config.objectType ?? config.ObjectType ?? stringRemote?.objectType ?? 0;
|
|
256
|
-
const objectInstanceRaw = config.objectInstance ?? config.ObjectInstance ?? stringRemote?.objectInstance ?? 0;
|
|
257
256
|
const propertyIdRaw = config.propertyId ?? config.PropertyId ?? stringRemote?.propertyId ?? 85;
|
|
258
257
|
const arrayIndexRaw = config.arrayIndex ?? config.ArrayIndex ?? stringRemote?.arrayIndex ?? null;
|
|
259
258
|
const valueType = String(config.valueType ?? config.ValueType ?? "e").toLowerCase();
|
|
260
259
|
|
|
261
260
|
const objectType = resolveEnumValue(this.bacnet?.enum?.ObjectType, objectTypeRaw, parseInteger(objectTypeRaw, 0));
|
|
262
|
-
const objectInstance = parseInteger(
|
|
261
|
+
const objectInstance = parseInteger(map.remoteAddress, null);
|
|
263
262
|
const propertyId = resolveEnumValue(this.bacnet?.enum?.PropertyIdentifier, propertyIdRaw, parseInteger(propertyIdRaw, 85));
|
|
264
263
|
const arrayIndex = parseInteger(arrayIndexRaw, null);
|
|
265
264
|
|
package/src/nodalis.js
CHANGED