Comprehensive grammar rewrite to conform to SCL specs. Improves parse success rate from 0% to 90% (187 of 208 reference project files). Case-insensitive keywords: - Add ci() helper for case-insensitive token matching - All keywords (TYPE, STRUCT, VAR, IF, etc.) now match any case - All type names (BOOL, Int, Dint, etc.) now match any case New grammar constructs: - RETURN statement - GOTO statement - AUTHOR/FAMILY/NAME block headers - VAR DB_SPECIFIC section - ARRAY[*] (variable-length arrays) - Multidimensional ARRAY[a..b, c..d] - REF_TO <type> - VARIANT type - Anonymous STRUCT...END_STRUCT as a type - Compound assignment operators (+=, -=, *=, /=, &=, |=, ^=) - Chained assignments (lvalue := lvalue := expression) - Bit access (lvalue.%X0) - Method calls (lvalue.field(...)) - Struct/array initializers ((TRUE, FALSE, ...), (name := value)) - END_STRUCT; (semicolon after END_STRUCT in UDTs) - Typed DB associated block name (DATA_BLOCK "Name" : "UDT") - BOM (Byte Order Mark) handling - TITLE as single-token statement (avoids keyword/identifier conflict) New type_builtin aliases: - LWORD, LINT, ULINT, UDINT, DATE, TIME_OF_DAY, DATE_AND_TIME - S5TIME, LTIME, DTL, LTOD, LDT, LTIME_OF_DAY, DATE_AND_LTIME New literal formats: - Binary (2#...), Octal (8#...) - BOOL#TRUE/FALSE - Typed integers (INT#, SINT#, DINT#, etc.) - Date/time literals (DATE#, TOD#, DT#, LTIME#, S5TIME#, DTL#) - WSTRING# and STRING# typed string literals - C# char literal - Time units with case-insensitive matching (T#1S, T#2s) Updated constructs: - CASE labels: comma-separated lists, string labels - Function call parameters: interleaved input (:=) and output (=>) params - FOR loop variable: accepts prefixed_identifier (#tempVar) - Array bounds: accept string/identifier constants (Array[0.."MAX"]) - Author/Family values: accept unquoted text with hyphens/slashes Known remaining issues (21 files, 10%): - Triple-nested parentheses in expressions - Some multi-line function call parameter patterns - END_REGION with trailing region name - Complex chained field access in assignments
tia-lsp - Language Server for Siemens TIA Portal
A standalone LSP (Language Server Protocol) server for Siemens TIA Portal languages. Currently supports SCL (Structured Control Language), with planned support for STL/AWL, LAD, and FBD.
The server speaks JSON-RPC over stdio and is editor-agnostic — usable from Neovim, VS Code, or any LSP client. For Neovim users, the companion plugin tia-lsp.nvim provides seamless integration (auto-prefix, blink.cmp source, workspace scanning, attribute toggle, etc.).
Features
LSP (Language Server Protocol)
| Feature | Description |
|---|---|
| Hover | Variable type and struct fields |
| Completion | # for variables, . for struct fields, ( for functions |
| Go to Definition | Jump to function, FB, DB, or UDT definition |
| Document Symbols | Shows functions, variables, types |
| Workspace Symbols | Search across all .scl files with fuzzy matching |
| Semantic Tokens | Keywords, variables highlighted |
| Inlay Hints | Shows variable types after declaration |
| External UDT | Load types from .plc.json or data_types/ folder |
Linter (Diagnostics)
| Feature | Description |
|---|---|
| Undefined Variables | Detects variables used without declaration |
| Invalid # Prefix | Detects # prefix in VAR sections |
| Unknown Types | Warns about unknown data types |
| Type Validation | Basic type checking |
Formatter
| Feature | Description |
|---|---|
| Document Formatting | Format entire SCL files |
| Multi-line Assignments | Preserves and formats multi-line expressions with proper alignment |
| FB Call Formatting | Formats function block calls in multiline style |
| Indentation | Proper block indentation |
| Keyword Casing | Maintains SCL keyword casing |
Tree-sitter Grammar
- Full SCL syntax support via tree-sitter
- Block definitions: ORGANIZATION_BLOCK, FUNCTION_BLOCK, FUNCTION
- Variable sections: VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, VAR_CONSTANT
- Control structures: IF/THEN/ELSIF/ELSE/END_IF, CASE/OF/END_CASE, FOR/TO/BY/DO/END_FOR, WHILE/END_WHILE, REPEAT/UNTIL/END_REPEAT
- Built-in data types: BOOL, INT, DINT, REAL, STRING, TIME, etc.
- Function block instances highlighted in calls
- FB parameter names (IN, PT, CU, etc.) highlighted as properties
#prefix for local variables highlighted- Operators: AND, OR, NOT, XOR, MOD
- Comments: line (
//), block ((* *)), C-style (/* */)
Running the Server
# Start the LSP server (reads JSON-RPC from stdin, writes to stdout)
lua src/main.lua
# Run server tests
make test
# or: lua src/main.lua --test
Mason Installation (Neovim)
For Neovim users, install via mason.nvim using a custom registry. See the tia-lsp.nvim README for full setup instructions.
Quick summary:
- Add the
tia-mason-registryas a lazy.nvim plugin - Register it with mason:
registries = { "file:~/.local/share/nvim/lazy/tia-mason-registry", "github:mason-org/mason-registry" } :MasonInstall tia-lsp
External UDT Support
Option 1: Create .plc.json
{
"dataTypes": [
{
"name": "equipmentData",
"struct": [
{"name": "id", "dataTypeName": "INT"},
{"name": "status", "dataTypeName": "BOOL"},
{"name": "temperature", "dataTypeName": "REAL"}
]
}
]
}
Option 2: Auto-scan data_types/ folder
The LSP automatically scans for SCL files in data_types/ folder and extracts TYPE definitions.
Option 3: Generate .plc.json
lua src/plc_json.lua --generate
Global Data Block Support
The server scans for .db files in the workspace and provides auto-completion for global data blocks and their members.
How it works
- Workspace Scanning: When opening a
.sclfile, the server scans the project for.dbfiles - DB Parsing: Parses
DATA_BLOCKdefinitions and extracts members fromVARsections - Auto-completion: Provides completion for DB names and members
Example
// Access global DB members
"HmiData".units
"Parameter".machine.unit100.paramTimeStampApplied
// Inside FB call parameter
#instParameterManager(hmiInterface:="HmiData".units, ...)
Building Tree-sitter Parser
# Generate parser from grammar.js
tree-sitter generate
# Run parser tests
tree-sitter test
# Parse a single SCL file
tree-sitter parse <file.scl>
Project Structure
tia-lsp/
├── src/ # LSP server (uses dofile)
│ ├── main.lua # Entry point, JSON-RPC protocol
│ ├── parser.lua # SCL parser (will become parser_scl.lua + dispatch)
│ ├── diagnostics.lua # Linter (source = "tia_lsp")
│ ├── formatter.lua # Document formatter
│ ├── treesitter.lua # Tree-sitter integration
│ ├── plc_json.lua # External UDT/DB loading (language-agnostic)
│ ├── builtin_instructions.lua # TIA Portal built-in types
│ ├── json.lua # JSON encoder/decoder
│ ├── grammar.json # Generated tree-sitter grammar
│ ├── node-types.json # Generated node types
│ ├── parser.c # Generated tree-sitter parser
│ └── tree_sitter/ # Tree-sitter C runtime headers
├── grammar.js # Tree-sitter grammar (language name: 'scl')
├── tree-sitter.json # Parser config
├── package.json # NPM scripts (tree-sitter)
├── Makefile # Build commands
├── generate_plc_json.lua # .plc.json generator utility
└── test/ # Server tests
├── run_tests.lua
├── test_harness.lua
├── test_formatter.lua
└── test_plc_json.lua
Architecture
The server (src/main.lua) communicates via JSON-RPC over stdio and handles:
- Text document synchronization
- Diagnostics (linting)
- Formatting
- Completion, hover, go-to-definition
- Semantic tokens
- Inlay hints
- Workspace symbols
The Neovim plugin (tia-lsp.nvim) manages the LSP client lifecycle, editor features, and workspace scanning. See its README for details.
SCL Code Example
FUNCTION_BLOCK "Em101Sequence"
VAR
x : INT;
myVar : equipmentData; // User-defined type
END_VAR
IF x > 5 THEN
myVar.id := 10;
END_IF
myVar.status := TRUE;
myVar.temperature := 25.5;
END_FUNCTION_BLOCK
Requirements
- Lua 5.1+ — LSP server runtime
- tree-sitter CLI — For regenerating the parser from
grammar.js(only needed during development) - Node.js — For tree-sitter CLI (only needed during development)
Testing
# Run server tests
lua test/run_tests.lua
# Run individual test files
lua test/test_formatter.lua
lua test/test_plc_json.lua
# Run LSP server in test mode
lua src/main.lua --test
Tests use files from a reference project (override with SCL_REF_PROJECT=/path/to/ref lua test/run_tests.lua).