diff --git a/.gitignore b/.gitignore index 48bc2c6..04ab8a4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /language/example/kBus/generated /language/example/etherCAT/generated /language/example/showcase/generated +/language/example/exhaustive/generated diff --git a/README.md b/README.md index 3caa03f..786f673 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ -# bcs-engineering-dsl +# BCS Engineering Framework -## 1. Prerequisites +## 1 Prerequisites - Windows with TcXaeShell Version 15.0 or higher -## 2. Example +## 2 Installation -To run the example in the folder [language/example](language/example), you need to follow the following steps. +To install the complete framework, you need to install three components: -### 2.1 Generate the artifacts +1. VS Code extension: `BCS Engineering DSL` +2. `bcs-engineering-cli` - The CLI to generate the artifacts from the DSL files. +3. `TcAutomation` - The tool to create and deploy the TwinCAT project from the generated artifacts. -For this follow the installation instructions in the [bcs-engineering-dsl](language/README.md) README file. +### 2.1 Install bcs-engineering-cli and VS Code Extension + +To install the `bcs-engineering-cli` and `BCS Engineering DSL` VS Code extension, follow the installation and usage instructions in the [README](language/README.md) file. ### 2.2 Install TcAutomation -_TcAutomation_ is responsible for the creation of the corresponding TwinCAT project given the generated artifacts. To install it, you need to run the `TcAutomationInstaller.exe`. +_TcAutomation_ is responsible for the creation and deployment of the corresponding TwinCAT project given the generated artifacts. To install it, you need to run the `TcAutomationInstaller.exe`. To create the installer, follow the steps described in the [TcAutomation](TcAutomation/README.md) README file. -### 2.3 Check TcAutomation Installation +#### Check TcAutomation Installation To check if _TcAutomation_ is installed correctly, you can run the following command in a command: @@ -25,20 +29,21 @@ To check if _TcAutomation_ is installed correctly, you can run the following com TcAutomation --help ``` -### 2.5 BCS Engineering CLI +## 3 Usage + +After installation, explore the [example](language/example) folder to get started with the framework. The examples demonstrate how to define control logic and hardware configurations using the BCS Engineering DSL. You can either generate some of the artifacts (ST and TwinCAT config files) without deployment or generate and deploy them to a Beckhoff device. -#### 2.5.1 Generate +#### 3.1 Generate -To generate the artifacts, you can run the following command in the `language/example` folder: +To generate the artifacts, you can run the following command in the `language/example` directory: ```bash bcs-engineering-cli beckhoff generate .\kBus\control.bcsctrl ``` -#### 2.5.2 Deploy +#### 3.2 Deploy -To deploy the generated artifacts, you can run the following command in the `language/example` folder. -(Note: this command generates and deploys the artifacts) +To generate and deploy the artifacts, you can run the following command in the `language/example` directory: ```bash bcs-engineering-cli beckhoff deploy .\kBus\control.bcsctrl diff --git a/TcAutomation/README.md b/TcAutomation/README.md index 5161035..82de767 100644 --- a/TcAutomation/README.md +++ b/TcAutomation/README.md @@ -1,16 +1,17 @@ -# Build +# TcAutomation Deployment Tool + +## 1 Prerequisites + +- [Inno Setup](https://jrsoftware.org/isinfo.php) + +## 2 Build Project ```bash msbuild /p:Configuration=Release TcAutomation.csproj ``` -# Windows Installer - -## Pre-requisites -- Build the project in Release mode. -- [Inno Setup](https://jrsoftware.org/isinfo.php) +## 3 Create Installer -## Build -- Open the `TcAutomation.iss` file in Inno Setup. +- Open the [TcAutomationInstaller.iss](TcAutomationInstaller.iss) file within the Inno Setup. - Click the "Build/Compile" button (or press Ctrl+F9)." -- The installer will be created in the `output` folder. \ No newline at end of file +- The installer will be created in the [output](output) folder. diff --git a/language/README.md b/language/README.md index 771ece7..1b03ee3 100644 --- a/language/README.md +++ b/language/README.md @@ -1,20 +1,22 @@ [![CI](https://github.com/elkoDev/bcs-engineering-dsl/actions/workflows/ci.yml/badge.svg)](https://github.com/elkoDev/bcs-engineering-dsl/actions/workflows/ci.yml) -# 1. Setup +# BCS Engineering DSL -## 1.1 Install Dependencies +## 1 Publishing + +### 1.1 Install Dependencies ```bash npm install ``` -## 1.2 Build +### 1.2 Build ```bash npm run langium:generate && npm run build ``` -## 1.3 Generate Extension +### 1.3 Generate Extension Before generating the extension, make sure to run the build commands first. @@ -22,14 +24,21 @@ Before generating the extension, make sure to run the build commands first. vsce package ``` -## 1.4 Publish CLI +### 1.4 Publish CLI +NOTE: Previous versions have been published under the name `bcs-engineering-dsl` on my personal npm account. To avoid confusion, you might want to publish under a different name. ```bash npm login npm publish --access public ``` -## 1.5 Install CLI +## 2 Installation + +### 2.1 Install VS Code Extension + +Right click on the `bcs-engineering-dsl-.vsix` file and select `Install Extension VSIX`. + +### 2.2 Install the CLI: ```bash npm install -g bcs-engineering-dsl @@ -41,15 +50,15 @@ or link the local version npm link ``` -# 2. Usage +## 3 Usage -## 2.1 Generate Code +### 3.1 Generate Code ```bash bcs-engineering-cli beckhoff generate ``` -## 2.2 Generate Code and Deploy +### 3.2 Generate Code and Deploy ```bash bcs-engineering-cli beckhoff deploy @@ -57,12 +66,14 @@ bcs-engineering-cli beckhoff deploy **NOTE:** The working directory will be set to the root of the provided file. This means that the .bcsctrl file can contain cross-references to other files in the same directory. -## 2.3 Supported Target Platforms +### 3.3 Supported Target Platforms Currently the following target platforms are supported: -### 2.3.1 Beckhoff TwinCAT 3 +#### 3.3.1 Beckhoff In addition to the standard libraries provided by Beckhoff, the following libraries are supported: -- `Tc3_DALI` +- `Tc3_DALI`: Please note that this library integration is not fully tested. + +## 4 Future Work diff --git a/language/bcs-engineering-dsl-0.0.22.vsix b/language/bcs-engineering-dsl-0.0.24.vsix similarity index 74% rename from language/bcs-engineering-dsl-0.0.22.vsix rename to language/bcs-engineering-dsl-0.0.24.vsix index 13c6360..337145e 100644 Binary files a/language/bcs-engineering-dsl-0.0.22.vsix and b/language/bcs-engineering-dsl-0.0.24.vsix differ diff --git a/language/example/showcase/showcase.bcsctrl b/language/example/showcase/showcase.bcsctrl index 2ac55dd..85340ec 100644 --- a/language/example/showcase/showcase.bcsctrl +++ b/language/example/showcase/showcase.bcsctrl @@ -1,7 +1,23 @@ control ShowcaseController { - var presenceSensor: BOOL; - var roomOccupancy: INT = 0; - unit CountPersonEntering once when (presenceSensor) { - roomOccupancy = roomOccupancy + 1; + enum OpMode { + Auto, + Manual, + Off + } + functionblock HeatingLogic { + inputs { + var iTemp: REAL; + var iMode: OpMode; + } + outputs { + var oHeating: BOOL; + } + logic { + oHeating = iMode == OpMode.Auto && iTemp < 20.5; + } + } + unit MorningWarmUp at TOD#06:00:00 { + var currentMode: OpMode = OpMode.Auto; + use HeatingLogic(iTemp: RoomSensor.Temperature, iMode: currentMode) -> HeatingActuator.IsActive; } } \ No newline at end of file diff --git a/language/example/showcase/showcase.bcshw b/language/example/showcase/showcase.bcshw index 8c349e3..c4fe046 100644 --- a/language/example/showcase/showcase.bcshw +++ b/language/example/showcase/showcase.bcshw @@ -33,7 +33,7 @@ controller ShowcaseController { channels: 4 } - portgroup TwoDigitalInputs { + portgroup Room1DigitalInputs { module: Zwei_Kanal_Digital_Eingang_Modul ioType: DIGITAL_INPUT channels: 2 @@ -50,7 +50,28 @@ controller ShowcaseController { ioType: ANALOG_OUTPUT channels: 4 } -} + + datapoint MotionSensor { + portgroup: Room1DigitalInputs + channels: { + channel Value: BOOL link "Channel 1^Input" + } + } + + datapoint RoomSensor { + portgroup: Room1DigitalInputs + channels: { + channel Temperature: REAL link "Channel 2^Input" + } + } + + datapoint HeatingActuator { + portgroup: TwoDigitalOutputs + channels: { + channel IsActive: BOOL link "Channel 1^Output" + } + } +} network { hostname: "CX-301714" diff --git a/language/package.json b/language/package.json index f8ea7eb..822d4b2 100644 --- a/language/package.json +++ b/language/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.22", + "version": "0.0.24", "name": "bcs-engineering-dsl", "displayName": "BCS Engineering DSL", "icon": "icon.png", diff --git a/language/puml/bcs-control-metamodel.puml b/language/puml/bcs-control-metamodel.puml index b3867c5..8f6288a 100644 --- a/language/puml/bcs-control-metamodel.puml +++ b/language/puml/bcs-control-metamodel.puml @@ -154,9 +154,9 @@ package "Literals" { ' Abstract element for what can be referenced by name abstract class NamedElement -NamedElement <|--- VarDecl -NamedElement <|--- EnumDecl -NamedElement <|--- Datapoint +NamedElement <|-- VarDecl +NamedElement <|-- EnumDecl +NamedElement <|-- Datapoint ' ===== RELATIONSHIPS ===== @@ -213,9 +213,9 @@ UseStmt "1" *-- "1" UseOutput : useOutput InputMapping "1" ..> "1" VarDecl : inputVar UseOutput "1" *-- "0..1" SimpleUseResult : singleOutput UseOutput "1" *-- "0..*" MappingUseResult : mappingOutputs -SimpleUseResult "1" ..> "1" VarDecl : targetOutputVar +SimpleUseResult "1" ..> "1" Ref : target MappingUseResult "1" ..> "1" VarDecl : fbOutputVar -MappingUseResult "1" ..> "1" VarDecl : targetOutputVar +MappingUseResult "1" ..> "1" Ref : target EdgeStmt "1" *-- "1" Ref : signal ' --- Expressions --- diff --git a/language/railroad/svg/bcs-control/MappingUseResult.svg b/language/railroad/svg/bcs-control/MappingUseResult.svg index 946692e..5766449 100644 --- a/language/railroad/svg/bcs-control/MappingUseResult.svg +++ b/language/railroad/svg/bcs-control/MappingUseResult.svg @@ -1,4 +1,4 @@ - + diff --git a/language/railroad/svg/bcs-control/SimpleUseResult.svg b/language/railroad/svg/bcs-control/SimpleUseResult.svg index cf140d2..f5c5b0a 100644 --- a/language/railroad/svg/bcs-control/SimpleUseResult.svg +++ b/language/railroad/svg/bcs-control/SimpleUseResult.svg @@ -1,4 +1,4 @@ - + diff --git a/language/src/cli/cli-util.ts b/language/src/cli/cli-util.ts index aa643d6..9a50dc5 100644 --- a/language/src/cli/cli-util.ts +++ b/language/src/cli/cli-util.ts @@ -13,14 +13,6 @@ import { isHardwareModel, } from "../language/generated/ast.js"; -/** - * Read a requirement document with the complete workspace (with requirements and - * tests) located in the folder of the file. - * @param fileName the main requirement model file - * @param services the language services - * @returns a tuple with the document indicated by the fileName and a list of - * documents from the workspace. - */ export async function extractDocuments( fileName: string, services: LangiumCoreServices, diff --git a/language/src/cli/main.ts b/language/src/cli/main.ts index 60efa2d..5b8cf2f 100644 --- a/language/src/cli/main.ts +++ b/language/src/cli/main.ts @@ -14,8 +14,6 @@ export default function () { program.version(pkg.version); program.addCommand(defineBeckhoffSubcommands()); - // program.addCommand(defineSiemensSubcommands()); - // program.addCommand(defineKnxSubcommands()); program.parse(); } diff --git a/language/src/cli/platform/beckhoff/application/main-program-generator.ts b/language/src/cli/platform/beckhoff/application/main-program-generator.ts index 2e58919..c21aac0 100644 --- a/language/src/cli/platform/beckhoff/application/main-program-generator.ts +++ b/language/src/cli/platform/beckhoff/application/main-program-generator.ts @@ -80,7 +80,6 @@ export class MainProgramGenerator { ); const declFilePath = path.join(this.destination, `MAIN_decl.st`); - // Ensure the destination directory exists fs.mkdirSync(this.destination, { recursive: true }); fs.writeFileSync(declFilePath, declContent); const implFilePath = path.join(this.destination, `MAIN_impl.st`); diff --git a/language/src/cli/platform/beckhoff/application/qualified_reference_name.ts b/language/src/cli/platform/beckhoff/application/qualified_reference_name.ts index 33c7079..78f4cca 100644 --- a/language/src/cli/platform/beckhoff/application/qualified_reference_name.ts +++ b/language/src/cli/platform/beckhoff/application/qualified_reference_name.ts @@ -25,9 +25,11 @@ export function getReferenceName(ref: Reference): string { /** * Check if a referenced element belongs to a control unit - * This helps us determine if we need to qualify the variable name + * @param ref Reference to check + * @return Tuple where the first element indicates if it's in a control unit, + * and the second element is the control unit name or null */ -export function isControlUnitVariable( +function isControlUnitVariable( ref: Reference ): [boolean, string | null] { const container = ref?.ref?.$container; diff --git a/language/src/cli/platform/beckhoff/application/statement-converter.ts b/language/src/cli/platform/beckhoff/application/statement-converter.ts index 21b46e3..b8d2943 100644 --- a/language/src/cli/platform/beckhoff/application/statement-converter.ts +++ b/language/src/cli/platform/beckhoff/application/statement-converter.ts @@ -25,7 +25,6 @@ import { } from "../../../../language/generated/ast.js"; import { ExpressionConverter } from "./expression-converter.js"; import { GlobalInstanceManager } from "./global-instance-manager.js"; -import { getQualifiedReferenceName } from "./qualified_reference_name.js"; /** * Converts DSL statements to IEC 61131-3 Structured Text and analyzes statement structures. @@ -120,14 +119,12 @@ export class StatementConverter { const fb = stmt.functionBlockRef.ref; const outs = fb ? getOutputs(fb) : []; const fbOut = outs.length === 1 ? outs[0].name : "output"; - const target = getQualifiedReferenceName( - stmt.useOutput.singleOutput.targetOutputVar - ); - return out + `${p}${target} := ${instanceName}.${fbOut};\n`; + const target = this.expr.emit(stmt.useOutput.singleOutput.target); + return out + `${p}${target} := ${instanceName}.${fbOut};\n`.trimEnd(); } for (const m of stmt.useOutput.mappingOutputs) { - const target = getQualifiedReferenceName(m.targetOutputVar); + const target = this.expr.emit(m.target); const fbOut = m.fbOutputVar.ref?.name ?? "output"; out += `${p}${target} := ${instanceName}.${fbOut};\n`; } diff --git a/language/src/cli/platform/index.ts b/language/src/cli/platform/index.ts index cd15347..73f2db9 100644 --- a/language/src/cli/platform/index.ts +++ b/language/src/cli/platform/index.ts @@ -4,16 +4,11 @@ import type { } from "../../language/generated/ast.js"; import { generate as genBeckhoff } from "./beckhoff/generate.js"; import { deploy as depBeckhoff } from "./beckhoff/deploy.js"; -//import { generate as genSiemens } from "./siemens/generator.js"; -//import { deploy as depSiemens } from "./siemens/deploy.js"; -//import { generate as genKnx } from "./knx/generator.js"; -//import { deploy as depKnx } from "./knx/deploy.js"; export type Platform = "Beckhoff" | "Siemens" | "KNX"; export interface GenerateResult { files: string[]; - // …any other metadata… } export interface DeployOptions { @@ -26,7 +21,6 @@ export interface DeployOptions { tcExe?: string; adsUsername?: string; adsPassword?: string; - // Siemens/KNX could have their options here } export function generateFor( @@ -40,10 +34,8 @@ export function generateFor( return genBeckhoff(control, hardware, destination); case "Siemens": throw new Error("Siemens generation not implemented yet"); - //return genSiemens(control, hardware, destination); case "KNX": throw new Error("KNX generation not implemented yet"); - //return genKnx(control, hardware, destination); default: throw new Error(`Unsupported platform: ${platform}`); } @@ -55,10 +47,8 @@ export function deployTo(platform: Platform, opts: DeployOptions): void { return depBeckhoff(opts); case "Siemens": throw new Error("Siemens deployment not implemented yet"); - //return depSiemens(opts); case "KNX": throw new Error("KNX deployment not implemented yet"); - //return depKnx(opts); default: throw new Error(`Unsupported platform: ${platform}`); } diff --git a/language/src/language/bcs-control.langium b/language/src/language/bcs-control.langium index 8ea541b..52691d9 100644 --- a/language/src/language/bcs-control.langium +++ b/language/src/language/bcs-control.langium @@ -227,11 +227,11 @@ UseOutput: ; SimpleUseResult: - targetOutputVar=[VarDecl] + target=Ref ; MappingUseResult: - fbOutputVar=[VarDecl] ':' targetOutputVar=[VarDecl] + fbOutputVar=[VarDecl] ':' target=Ref ; diff --git a/language/src/language/control/bcs-control-lang-completion-provider.ts b/language/src/language/control/bcs-control-lang-completion-provider.ts index 75f75df..02150f8 100644 --- a/language/src/language/control/bcs-control-lang-completion-provider.ts +++ b/language/src/language/control/bcs-control-lang-completion-provider.ts @@ -21,7 +21,7 @@ export class BCSControlLangCompletionProvider extends DefaultCompletionProvider ): MaybePromise { const node = context.node; - if (next.property === "property" && isRef(node)) { + if (next.property === "properties" && isRef(node)) { const namedElement = node.ref.ref; if (isDatapoint(namedElement)) { diff --git a/language/src/language/control/bcs-control-lang-formatter.ts b/language/src/language/control/bcs-control-lang-formatter.ts index 841ede7..1b5ffa2 100644 --- a/language/src/language/control/bcs-control-lang-formatter.ts +++ b/language/src/language/control/bcs-control-lang-formatter.ts @@ -59,7 +59,8 @@ export class BCSControlLangFormatter extends AbstractFormatter { return true; } return false; - } private formatStatements(node: AstNode): boolean { + } + private formatStatements(node: AstNode): boolean { if (ast.isAssignmentStmt(node)) { this.formatAssignmentStmt(node); return true; @@ -290,7 +291,8 @@ export class BCSControlLangFormatter extends AbstractFormatter { // Format closing brace closeBrace.prepend(Formatting.newLine()); - } private formatIfStmt(node: ast.IfStmt): void { + } + private formatIfStmt(node: ast.IfStmt): void { const formatter = this.getNodeFormatter(node); const openParen = formatter.keyword("("); const closeParen = formatter.keyword(")"); @@ -301,9 +303,9 @@ export class BCSControlLangFormatter extends AbstractFormatter { openParen.prepend(Formatting.oneSpace()); openParen.append(Formatting.noSpace()); closeParen.prepend(Formatting.noSpace()); - closeParen.append(Formatting.oneSpace()); // Consistent space after closing paren + closeParen.append(Formatting.oneSpace()); - // Format braces - no additional space before brace since we have space after paren + // Format braces openBrace.prepend(Formatting.noSpace()); openBrace.append(Formatting.newLine()); @@ -322,14 +324,15 @@ export class BCSControlLangFormatter extends AbstractFormatter { if (node.elseStmt) { formatter.node(node.elseStmt).prepend(Formatting.oneSpace()); } - } private formatElseIfStmt(node: ast.ElseIfStmt): void { + } + private formatElseIfStmt(node: ast.ElseIfStmt): void { const formatter = this.getNodeFormatter(node); const openParen = formatter.keyword("("); const closeParen = formatter.keyword(")"); const openBrace = formatter.keyword("{"); const closeBrace = formatter.keyword("}"); - // Format 'else if' keywords - ensure proper spacing + // Format 'else if' keywords formatter.keyword("else").append(Formatting.oneSpace()); formatter.keyword("if").append(Formatting.noSpace()); @@ -337,9 +340,9 @@ export class BCSControlLangFormatter extends AbstractFormatter { openParen.prepend(Formatting.noSpace()); openParen.append(Formatting.noSpace()); closeParen.prepend(Formatting.noSpace()); - closeParen.append(Formatting.oneSpace()); // Consistent space after closing paren + closeParen.append(Formatting.oneSpace()); - // Format braces - no additional space before brace + // Format braces openBrace.prepend(Formatting.noSpace()); openBrace.append(Formatting.newLine()); @@ -367,7 +370,8 @@ export class BCSControlLangFormatter extends AbstractFormatter { // Format closing brace closeBrace.prepend(Formatting.newLine()); - } private formatWhileStmt(node: ast.WhileStmt): void { + } + private formatWhileStmt(node: ast.WhileStmt): void { const formatter = this.getNodeFormatter(node); const openParen = formatter.keyword("("); const closeParen = formatter.keyword(")"); @@ -378,9 +382,9 @@ export class BCSControlLangFormatter extends AbstractFormatter { openParen.prepend(Formatting.oneSpace()); openParen.append(Formatting.noSpace()); closeParen.prepend(Formatting.noSpace()); - closeParen.append(Formatting.oneSpace()); // Consistent space after closing paren + closeParen.append(Formatting.oneSpace()); - // Format braces - no additional space before brace + // Format braces openBrace.prepend(Formatting.noSpace()); openBrace.append(Formatting.newLine()); @@ -389,7 +393,8 @@ export class BCSControlLangFormatter extends AbstractFormatter { // Format closing brace closeBrace.prepend(Formatting.newLine()); - } private formatForStmt(node: ast.ForStmt): void { + } + private formatForStmt(node: ast.ForStmt): void { const formatter = this.getNodeFormatter(node); const openParen = formatter.keyword("("); const closeParen = formatter.keyword(")"); @@ -400,12 +405,12 @@ export class BCSControlLangFormatter extends AbstractFormatter { openParen.prepend(Formatting.oneSpace()); openParen.append(Formatting.noSpace()); closeParen.prepend(Formatting.noSpace()); - closeParen.append(Formatting.oneSpace()); // Consistent space after closing paren + closeParen.append(Formatting.oneSpace()); // Format 'to' and 'by' keywords formatter.keyword("to").surround(Formatting.oneSpace()); formatter.keyword("by").surround(Formatting.oneSpace()); - // Format braces - no additional space before brace + // Format braces openBrace.prepend(Formatting.noSpace()); openBrace.append(Formatting.newLine()); @@ -653,10 +658,10 @@ export class BCSControlLangFormatter extends AbstractFormatter { const openBrace = formatter.keyword("{"); const closeBrace = formatter.keyword("}"); - // Format 'inputs' keyword - space before opening brace + // Format 'inputs' keyword formatter.keyword("inputs").append(Formatting.noSpace()); - // Format braces - consistent with other blocks + // Format braces openBrace.prepend(Formatting.oneSpace()); openBrace.append(Formatting.newLine()); @@ -672,10 +677,10 @@ export class BCSControlLangFormatter extends AbstractFormatter { const openBrace = formatter.keyword("{"); const closeBrace = formatter.keyword("}"); - // Format 'outputs' keyword - space before opening brace + // Format 'outputs' keyword formatter.keyword("outputs").append(Formatting.noSpace()); - // Format braces - consistent with other blocks + // Format braces openBrace.prepend(Formatting.oneSpace()); openBrace.append(Formatting.newLine()); @@ -691,10 +696,10 @@ export class BCSControlLangFormatter extends AbstractFormatter { const openBrace = formatter.keyword("{"); const closeBrace = formatter.keyword("}"); - // Format 'locals' keyword - space before opening brace + // Format 'locals' keyword formatter.keyword("locals").append(Formatting.noSpace()); - // Format braces - consistent with other blocks + // Format braces openBrace.prepend(Formatting.oneSpace()); openBrace.append(Formatting.newLine()); @@ -710,14 +715,14 @@ export class BCSControlLangFormatter extends AbstractFormatter { const openBrace = formatter.keyword("{"); const closeBrace = formatter.keyword("}"); - // Format 'logic' keyword - space before opening brace + // Format 'logic' keyword formatter.keyword("logic").append(Formatting.noSpace()); - // Format braces - consistent with other blocks + // Format braces openBrace.prepend(Formatting.oneSpace()); openBrace.append(Formatting.newLine()); - // Indent logic statements - this is crucial for proper else statement formatting + // Indent logic statements formatter.interior(openBrace, closeBrace).prepend(Formatting.indent()); // Format closing brace diff --git a/language/src/language/control/bcs-control-lang-semantic-token-provider.ts b/language/src/language/control/bcs-control-lang-semantic-token-provider.ts index ce691e6..8b9b567 100644 --- a/language/src/language/control/bcs-control-lang-semantic-token-provider.ts +++ b/language/src/language/control/bcs-control-lang-semantic-token-provider.ts @@ -21,7 +21,6 @@ import { isStructFieldDecl, isUseStmt, isMappingUseResult, - isSimpleUseResult, isInputMapping, isEnumMemberLiteral, isCaseLiteral, @@ -213,24 +212,12 @@ export class BCSControlLangSemanticTokenProvider extends AbstractSemanticTokenPr property: "fbOutputVar", type: SemanticTokenTypes.parameter, }); - acceptor({ - node, - property: "targetOutputVar", - type: SemanticTokenTypes.variable, - }); - } - if (isSimpleUseResult(node)) { - acceptor({ - node, - property: "targetOutputVar", - type: SemanticTokenTypes.variable, - }); } if (isInputMapping(node)) { acceptor({ node, property: "inputVar", - type: SemanticTokenTypes.comment, + type: SemanticTokenTypes.parameter, }); } if (isEnumMemberLiteral(node)) { diff --git a/language/src/language/control/utils/expression-utils.ts b/language/src/language/control/utils/expression-utils.ts index e825640..11399f9 100644 --- a/language/src/language/control/utils/expression-utils.ts +++ b/language/src/language/control/utils/expression-utils.ts @@ -3,7 +3,7 @@ * Contains methods for stringifying expressions and manipulating expression data. */ export class ExpressionUtils { - static stringifyExpression(expr: any): string { + public static stringifyExpression(expr: any): string { if (!expr) return "undefined"; switch (expr.$type) { @@ -24,14 +24,12 @@ export class ExpressionUtils { case "Primary": if (expr.val) { - if (Array.isArray(expr.val?.elements)) { - // It's an ArrayLiteral + if (this.isArrayLiteral(expr)) { return `[${expr.val.elements .map((e: any) => this.stringifyExpression(e)) .join(", ")}]`; } - if (Array.isArray(expr.val?.fields)) { - // It's a StructLiteral + if (this.isStructLiteral(expr)) { return `{${expr.val.fields .map( (f: any) => `${f.name}: ${this.stringifyExpression(f.value)}` @@ -46,4 +44,12 @@ export class ExpressionUtils { return `[${expr.$type}]`; } } + + private static isArrayLiteral(expr: any): boolean { + return expr?.$type === "Primary" && Array.isArray(expr.val?.elements); + } + + private static isStructLiteral(expr: any): boolean { + return expr?.$type === "Primary" && Array.isArray(expr.val?.fields); + } } diff --git a/language/src/language/control/utils/usestmt-validation-utils.ts b/language/src/language/control/utils/usestmt-validation-utils.ts index a6f16d9..bc826b6 100644 --- a/language/src/language/control/utils/usestmt-validation-utils.ts +++ b/language/src/language/control/utils/usestmt-validation-utils.ts @@ -1,8 +1,16 @@ import { ValidationAcceptor } from "langium"; -import { UseStmt, FunctionBlockDecl, UseOutput } from "../../generated/ast.js"; +import { + UseStmt, + FunctionBlockDecl, + UseOutput, + isDatapoint, + isEnumDecl, + Ref, +} from "../../generated/ast.js"; import { getInputs, getOutputs } from "./function-block-utils.js"; import { DuplicationValidationUtils } from "./duplication-validation-utils.js"; import { TypeInferenceUtils } from "./type-inference-utils.js"; +import { ExpressionUtils } from "./expression-utils.js"; /** * Utility class for validating UseStmt (function block usage) operations. @@ -130,11 +138,11 @@ export class UseStmtValidationUtils { } const expected = getOutputs(fb)[0]; - const actual = output.singleOutput!.targetOutputVar?.ref; + const actualTarget = output.singleOutput!.target; - if (actual) { + if (UseStmtValidationUtils.checkTargetIsAssignable(actualTarget, accept)) { const expectedType = TypeInferenceUtils.inferVarDeclType(expected); - const actualType = TypeInferenceUtils.inferVarDeclType(actual); + const actualType = TypeInferenceUtils.inferType(actualTarget, accept); if ( expectedType && @@ -143,8 +151,12 @@ export class UseStmtValidationUtils { ) { accept( "error", - `Type mismatch for output '${expected.name}': cannot assign to '${actual.name}' of type '${actualType}', expected '${expectedType}'.`, - { node: output.singleOutput!, property: "targetOutputVar" } + `Type mismatch for output '${ + expected.name + }': cannot assign to '${ExpressionUtils.stringifyExpression( + actualTarget + )}' of type '${actualType}', expected '${expectedType}'.`, + { node: output.singleOutput!, property: "target" } ); } } @@ -180,25 +192,63 @@ export class UseStmtValidationUtils { // Perform type checking for mappings for (const map of output.mappingOutputs) { const fbOutputVar = map.fbOutputVar?.ref; - const targetOutputVar = map.targetOutputVar?.ref; + const targetRef = map.target; - if (!targetOutputVar || !fbOutputVar) continue; + if (!targetRef || !fbOutputVar) continue; - const expected = getOutputs(fb).find((o) => o.name === fbOutputVar.name); - const expectedType = expected ? TypeInferenceUtils.inferVarDeclType(expected) : undefined; - const actualType = TypeInferenceUtils.inferVarDeclType(targetOutputVar); + if (UseStmtValidationUtils.checkTargetIsAssignable(targetRef, accept)) { + const expected = getOutputs(fb).find( + (o) => o.name === fbOutputVar.name + ); + const expectedType = expected + ? TypeInferenceUtils.inferVarDeclType(expected) + : undefined; + const actualType = TypeInferenceUtils.inferType(targetRef, accept); + + if ( + expectedType && + actualType && + !TypeInferenceUtils.isTypeAssignable(expectedType, actualType) + ) { + accept( + "error", + `Type mismatch: cannot assign output '${ + fbOutputVar.name + }' to '${ExpressionUtils.stringifyExpression( + targetRef + )}'. Expected assignable type for '${expectedType}', but got '${actualType}'.`, + { node: map, property: "target" } + ); + } + } + } + } - if ( - expectedType && - actualType && - !TypeInferenceUtils.isTypeAssignable(expectedType, actualType) - ) { + private static checkTargetIsAssignable( + target: Ref, + accept: ValidationAcceptor + ): boolean { + const targetRef = target.ref?.ref; + if (!targetRef) return false; + + if (isEnumDecl(targetRef)) { + accept("error", "Cannot assign a 'use' output to an enum type.", { + node: target, + }); + return false; + } + + if (isDatapoint(targetRef)) { + const portGroup = targetRef.portgroup?.ref; + if (portGroup?.ioType.includes("INPUT")) { accept( "error", - `Type mismatch for mapped output '${fbOutputVar.name}': expected '${expectedType}', got '${actualType}'.`, - { node: map, property: "fbOutputVar" } + `Cannot assign a 'use' output to a read-only input channel.`, + { node: target } ); + return false; } } + return true; } } diff --git a/language/test/validating/validating.test.ts b/language/test/validating/validating.test.ts index b939813..adfea3a 100644 --- a/language/test/validating/validating.test.ts +++ b/language/test/validating/validating.test.ts @@ -132,7 +132,7 @@ describe("BCS Control Validation Tests", () => { // 6. Output type mismatch (single) "Type mismatch for output 'oHeating': cannot assign to 'isHeating_should_fail_type_matching' of type 'INT', expected 'BOOL'.", // 7. Output type mismatch (mapping) - "Type mismatch for mapped output 'oHeating': expected 'BOOL', got 'INT'.", + "Type mismatch: cannot assign output 'oHeating' to 'isHeating_should_fail_type_matching'. Expected assignable type for 'BOOL', but got 'INT'.", // 8. Output mapping could not resolve "Could not resolve reference to VarDecl named 'oExtra'.", "Could not resolve reference to VarDecl named 'oWrong'.",