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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodalis-compiler",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.",
5
5
  "icon": "nodalis.png",
6
6
  "main": "src/nodalis.js",
@@ -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 lines = sourceCode.split('\n');
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 lines = sourceCode.split("\n");
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 lines = sourceCode.split("\n");
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
- return [`// unsupported: ${stmt.type}`];
182
+ throw createTranspileError(`Unsupported statement type '${stmt.type}'`, stmt);
183
183
  }
184
184
  }
185
185
  catch(e){
186
- console.error(e + "\n" + JSON.stringify(stmt));
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
- return [`// Unsupported statement type: ${stmt.type}`];
199
+ throw createTranspileError(`Unsupported statement type '${stmt.type}'`, stmt);
200
200
  }
201
201
  } catch (e) {
202
- console.error("Error transpiling statement", stmt, e);
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 new Error(`Expected '${value}', but got '${token?.value}'`);
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 parseProgram();
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 new Error(`Expected address after AT, got '${addrToken?.value}'`);
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
- consume(); // Skip unknown
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 parseProgram() {
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
- // Remove single-line comments (//...)
44
- code = code.replace(/\/\/.*$/gm, '');
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(code)) !== null) {
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(objectInstanceRaw, 0);
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
@@ -104,7 +104,8 @@ export class Nodalis {
104
104
  listProgrammers() {
105
105
  return this.programmers.map(p => ({
106
106
  name: p.name,
107
- target: p.target
107
+ target: p.target,
108
+ required: p.required
108
109
  }));
109
110
  }
110
111
 
@@ -23,6 +23,7 @@ export class ArduinoProgrammer extends Programmer {
23
23
  super(options);
24
24
  this.name = 'ArduinoProgrammer';
25
25
  this.target = 'arduino';
26
+ this.required = ["fqbn"];
26
27
  }
27
28
 
28
29
  async program() {
@@ -32,6 +32,7 @@ export class FileProgrammer extends Programmer {
32
32
  super(options);
33
33
  this.name = 'FileProgrammer';
34
34
  this.target = 'FILE';
35
+ this.required = [];
35
36
  }
36
37
 
37
38
  async program() {
@@ -26,6 +26,7 @@ export class MTIProgrammer extends Programmer {
26
26
  super(options);
27
27
  this.name = "MTIProgrammer";
28
28
  this.target = "MTI";
29
+ this.required = [];
29
30
  }
30
31
 
31
32
  async program() {
@@ -37,6 +37,7 @@ export class SSHProgrammer extends Programmer {
37
37
  super(options);
38
38
  this.name = 'SSHProgrammer';
39
39
  this.target = 'SSH';
40
+ this.required = ["username", "password"];
40
41
  }
41
42
 
42
43
  async program() {