nodalis-compiler 1.0.23 → 1.0.24

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.24] 2026-03-04
4
+ - Added support for CONFIGURATION, RESOURCE, TASK, and PROGRAM (instance) keywords in ST.
5
+ - Added "required" arrays to programmers for communicating required parameters beyond the base params when calling program.
6
+
3
7
  ## [1.0.23] 2026-03-03
4
8
  - Changed IEC parser to interpret Function Blocks that are actually standard functions to a formal function call.
5
9
  - 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.24",
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());
@@ -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());
@@ -32,6 +32,15 @@ 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];
@@ -55,19 +64,101 @@ export function parseStructuredText(code) {
55
64
 
56
65
  switch (token.value.toUpperCase()) {
57
66
  case 'PROGRAM':
58
- return parseProgram();
67
+ return parseProgramOrInstance();
59
68
  case 'FUNCTION':
60
69
  return parseFunction();
61
70
  case 'FUNCTION_BLOCK':
62
71
  return parseFunctionBlock();
63
72
  case 'VAR_GLOBAL':
64
73
  return parseGlobalVarSection();
74
+ case 'TASK':
75
+ return parseTask();
76
+ case 'CONFIGURATION':
77
+ return parseContainer('CONFIGURATION', 'END_CONFIGURATION');
78
+ case 'RESOURCE':
79
+ return parseContainer('RESOURCE', 'END_RESOURCE');
65
80
  default:
66
81
  consume();
67
82
  return null;
68
83
  }
69
84
  }
70
85
 
86
+ function parseContainer(startKeyword, endKeyword) {
87
+ expect(startKeyword);
88
+ const blocks = [];
89
+
90
+ while (peek() && peek().value.toUpperCase() !== endKeyword) {
91
+ const upper = peek().value.toUpperCase();
92
+ if (TOP_LEVEL_STARTERS.has(upper)) {
93
+ const block = parseBlock();
94
+ if (Array.isArray(block)) blocks.push(...block);
95
+ else if (block) blocks.push(block);
96
+ } else {
97
+ consume();
98
+ }
99
+ }
100
+
101
+ expect(endKeyword);
102
+ return blocks;
103
+ }
104
+
105
+ function parseTask() {
106
+ expect('TASK');
107
+ const name = consume().value;
108
+ let interval = '1000';
109
+ let priority = '1';
110
+
111
+ if (peek()?.value === '(') {
112
+ consume();
113
+ while (peek() && peek().value !== ')') {
114
+ const key = consume().value.toUpperCase();
115
+ if (peek()?.value === ':=') consume();
116
+
117
+ const valueTokens = [];
118
+ while (peek() && peek().value !== ',' && peek().value !== ')') {
119
+ valueTokens.push(consume().value);
120
+ }
121
+ const value = valueTokens.join('');
122
+
123
+ if (key === 'INTERVAL') interval = normalizeTaskInterval(value);
124
+ else if (key === 'PRIORITY') priority = normalizeNumericString(value, '1');
125
+
126
+ if (peek()?.value === ',') consume();
127
+ }
128
+ expect(')');
129
+ }
130
+
131
+ if (peek()?.value === ';') consume();
132
+ return { type: 'TaskDeclaration', name, interval, priority };
133
+ }
134
+
135
+ function normalizeNumericString(value, fallback) {
136
+ const match = String(value || '').match(/-?\d+/);
137
+ return match ? match[0] : fallback;
138
+ }
139
+
140
+ function normalizeTaskInterval(value) {
141
+ const text = String(value || '').trim();
142
+ if (!text) return '1000';
143
+
144
+ if (/^-?\d+$/.test(text)) return text;
145
+
146
+ const duration = text.match(/^T#?([+-]?\d+(?:\.\d+)?)(MS|S|M|H|D)?$/i);
147
+ if (!duration) return normalizeNumericString(text, '1000');
148
+
149
+ const amount = Number(duration[1]);
150
+ const unit = (duration[2] || 'MS').toUpperCase();
151
+ const multipliers = {
152
+ MS: 1,
153
+ S: 1000,
154
+ M: 60000,
155
+ H: 3600000,
156
+ D: 86400000
157
+ };
158
+ const ms = Math.round(amount * (multipliers[unit] || 1));
159
+ return String(ms);
160
+ }
161
+
71
162
  function parseGlobalVarSection() {
72
163
  expect('VAR_GLOBAL');
73
164
  const variables = [];
@@ -342,9 +433,22 @@ function parseStatementsUntil(endTokens) {
342
433
  return peek(1)?.value === ':' || peek(1)?.value === ',';
343
434
  }
344
435
 
345
- function parseProgram() {
436
+ function parseProgramOrInstance() {
346
437
  expect('PROGRAM');
347
438
  const name = consume().value;
439
+
440
+ if (peek()?.value?.toUpperCase() === 'WITH' || peek()?.value === ':') {
441
+ let associatedTaskName = '';
442
+ if (peek()?.value?.toUpperCase() === 'WITH') {
443
+ consume();
444
+ associatedTaskName = consume().value;
445
+ }
446
+ expect(':');
447
+ const typeName = consume().value;
448
+ if (peek()?.value === ';') consume();
449
+ return { type: 'ProgramInstanceDeclaration', name, typeName, associatedTaskName };
450
+ }
451
+
348
452
  const vars = [];
349
453
  const stmts = [];
350
454
 
@@ -395,8 +499,21 @@ function parseStatementsUntil(endTokens) {
395
499
  const body = [];
396
500
  while (position < tokens.length) {
397
501
  const block = parseBlock();
398
- if (block) body.push(block);
502
+ if (Array.isArray(block)) body.push(...block);
503
+ else if (block) body.push(block);
399
504
  }
400
505
 
401
506
  return { type: 'Program', body };
402
507
  }
508
+
509
+ export function buildCompilerMetadataDirectives(ast) {
510
+ const lines = [];
511
+ for (const block of ast?.body || []) {
512
+ if (block?.type === 'TaskDeclaration') {
513
+ lines.push(`//Task={"Name":"${block.name}", "Interval":"${block.interval}", "Priority":"${block.priority}"}`);
514
+ } else if (block?.type === 'ProgramInstanceDeclaration') {
515
+ lines.push(`//Instance={"TypeName":"${block.typeName}", "Name":"${block.name}", "AssociatedTaskName":"${block.associatedTaskName || ''}"}`);
516
+ }
517
+ }
518
+ return lines.join('\n') + (lines.length > 0 ? '\n' : '');
519
+ }
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() {