UNPKG

nodalis-compiler

Version:

Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.

367 lines (317 loc) 10 kB
/* eslint-disable curly */ /* eslint-disable eqeqeq */ // Copyright [2025] Nathan Skipper // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @description Structured Text Parser * @author Nathan Skipper, MTI * @version 1.0.2 * @copyright Apache 2.0 */ import { tokenize } from './tokenizer.js'; import {mapType} from "./gcctranspiler.js"; /** * Parses a block of structured text code and divides it into statement objects that can then be transpiled. * @param {string} code A block of Structured Text Code * @returns {[]} An array of tokenized and parsed statements. */ export function parseStructuredText(code) { const tokens = tokenize(code); let position = 0; function peek(offset = 0) { return tokens[position + offset]; } function consume() { return tokens[position++]; } function expect(value) { const token = consume(); if (!token || token.value.toUpperCase() !== value.toUpperCase()) { throw new Error(`Expected '${value}', but got '${token?.value}'`); } return token; } function parseBlock() { const token = peek(); if (!token) return null; switch (token.value.toUpperCase()) { case 'PROGRAM': return parseProgram(); case 'FUNCTION': return parseFunction(); case 'FUNCTION_BLOCK': return parseFunctionBlock(); case 'VAR_GLOBAL': return parseGlobalVarSection(); default: consume(); return null; } } function parseGlobalVarSection() { expect('VAR_GLOBAL'); const variables = []; while (peek() && peek().value.toUpperCase() !== 'END_VAR') { const name = consume().value; let address = null; let token = peek(); if (peek()?.value.toUpperCase?.() === 'AT') { consume(); // skip 'AT' const addrToken = consume(); if (addrToken?.type === 'ADDRESS' || addrToken?.type === 'IDENTIFIER') { address = addrToken.value; } else { throw new Error(`Expected address after AT, got '${addrToken?.value}'`); } } expect(':'); const type = consume().value; let initialValue = null; if (peek()?.value === ':=') { consume(); // consume ':=' initialValue = consume().value; } variables.push({ name, type, address, initialValue, sectionType: 'VAR_GLOBAL' }); if (peek()?.value === ';') consume(); } expect('END_VAR'); return { type: 'GlobalVars', variables }; } function parseVarSection() { const variables = []; const sectionType = consume().value.toUpperCase(); while (peek() && peek().value.toUpperCase() !== 'END_VAR') { const name = consume().value; expect(':'); const type = consume().value; let initialValue = null; if (peek()?.value === ':=') { consume(); // consume ':=' initialValue = consume().value; } variables.push({ name, type, initialValue, sectionType }); if (peek()?.value === ';') consume(); } expect('END_VAR'); return variables; } function parseStatements(until) { const statements = []; while (peek() && peek().value.toUpperCase() !== until) { const stmt = parseStatement(); if (stmt) statements.push(stmt); } return statements; } function parseStatement() { const token = peek(); if (!token) return null; if (token.value.toUpperCase() === 'IF') return parseIf(); if (token.value.toUpperCase() === 'WHILE') return parseWhile(); if (token.value.toUpperCase() === 'FOR') return parseFor(); if (token.value.toUpperCase() === 'REPEAT') return parseRepeat(); if (token.value.toUpperCase() === 'CASE') return parseCase(); // Assignment: x := y; const lhsTokens = []; let i = 0; while (peek(i) && peek(i).value !== ':=' && peek(i).value !== ';') { lhsTokens.push(peek(i)); i++; } if (peek(i)?.value === ':=') { const lhs = lhsTokens.map(t => t.value).join(''); for (let j = 0; j < i + 1; j++) consume(); // consume LHS and := const right = []; while (peek() && peek().value !== ';') { right.push(consume().value); } if (peek()?.value === ';') consume(); return { type: 'ASSIGN', left: lhs, right }; } // Function block call like: T1(); if (token.value && peek(1)?.value === '(' && peek(2)?.value === ')') { const name = consume().value; consume(); // ( consume(); // ) if (peek()?.value === ';') consume(); return { type: 'CALL', name }; } consume(); // Skip unknown return null; } function parseIf() { consume(); // IF // Collect condition tokens until THEN const conditionTokens = []; while (peek() && peek().value.toUpperCase() !== 'THEN') { conditionTokens.push(consume().value); } consume(); // THEN const thenBlock = parseStatementsUntil(['ELSIF', 'ELSE', 'END_IF']); const elseIfBlocks = []; let elseBlock = null; while (peek()?.value.toUpperCase() === 'ELSIF') { consume(); // ELSIF const elifCondTokens = []; while (peek() && peek().value.toUpperCase() !== 'THEN') { elifCondTokens.push(consume().value); } consume(); // THEN const elifBlock = parseStatementsUntil(['ELSIF', 'ELSE', 'END_IF']); elseIfBlocks.push({ condition: elifCondTokens, block: elifBlock }); } if (peek()?.value.toUpperCase() === 'ELSE') { consume(); // ELSE elseBlock = parseStatementsUntil(['END_IF']); } if (peek()?.value.toUpperCase() === 'END_IF') { consume(); // END_IF } return { type: 'IF', condition: conditionTokens, thenBlock, elseIfBlocks, elseBlock }; } function parseStatementsUntil(endTokens) { const statements = []; while (peek() && !endTokens.includes(peek().value.toUpperCase())) { const stmt = parseStatement(); if (stmt) { statements.push(stmt); } else { console.warn('⚠️ Unrecognized statement at token:', peek()); consume(); // prevent infinite loop } } return statements; } function parseWhile() { consume(); // WHILE const condition = []; while (peek() && peek().value.toUpperCase() !== 'DO') { condition.push(consume().value); } expect('DO'); const body = parseStatements('END_WHILE'); expect('END_WHILE'); return { type: 'WHILE', condition, body }; } function parseFor() { consume(); // FOR const variable = consume().value; expect(':='); const from = consume().value; expect('TO'); const to = consume().value; let step = '1'; if (peek()?.value.toUpperCase() === 'BY') { consume(); step = consume().value; } expect('DO'); const body = parseStatements('END_FOR'); expect('END_FOR'); return { type: 'FOR', variable, from, to, step, body }; } function parseRepeat() { consume(); // REPEAT const body = parseStatements('UNTIL'); expect('UNTIL'); const condition = []; while (peek() && peek().value !== ';') { condition.push(consume().value); } if (peek()?.value === ';') consume(); return { type: 'REPEAT', condition, body }; } function parseCase() { consume(); // CASE const expression = []; while (peek() && peek().value.toUpperCase() !== 'OF') { expression.push(consume().value); } expect('OF'); const branches = []; while (peek() && peek().value.toUpperCase() !== 'END_CASE') { const label = consume().value; expect(':'); const body = parseStatements('ELSE'); branches.push({ label, body }); } expect('END_CASE'); return { type: 'CASE', expression, branches }; } function parseProgram() { expect('PROGRAM'); const name = consume().value; const vars = []; const stmts = []; while (peek() && peek().value.toUpperCase().startsWith('VAR')) { vars.push(...parseVarSection()); } vars.forEach((v) => { if(mapType(v.type) === "auto"){ stmts.push({type: "CALL", name: v.name}); } }); stmts.push(...parseStatements('END_PROGRAM')); expect('END_PROGRAM'); return { type: 'ProgramDeclaration', name, varSections: vars, statements: stmts }; } function parseFunction() { expect('FUNCTION'); const name = consume().value; expect(':'); const returnType = consume().value; const vars = []; const stmts = []; while (peek() && peek().value.toUpperCase().startsWith('VAR')) { vars.push(...parseVarSection()); } vars.forEach((v) => { if(mapType(v.type) === "auto"){ stmts.push({type: "CALL", name: v.name}); } }); stmts.push(...parseStatements('END_FUNCTION')); expect('END_FUNCTION'); return { type: 'FunctionDeclaration', name, returnType, varSections: vars, statements: stmts }; } function parseFunctionBlock() { expect('FUNCTION_BLOCK'); const name = consume().value; const vars = []; const stmts = []; while (peek() && peek().value.toUpperCase().startsWith('VAR')) { vars.push(...parseVarSection()); } vars.forEach((v) => { if(mapType(v.type) === "auto"){ stmts.push({type: "CALL", name: v.name}); } }); stmts.push(...parseStatements('END_FUNCTION_BLOCK')); expect('END_FUNCTION_BLOCK'); return { type: 'FunctionBlockDeclaration', name, varSections: vars, statements: stmts }; } const body = []; while (position < tokens.length) { const block = parseBlock(); if (block) body.push(block); } return { type: 'Program', body }; }