diagnostics.lua: - Add VAR DB_SPECIFIC to section detection - Make all VAR/END_VAR detection case-insensitive (matches grammar) - Replace 20-line hardcoded type check list with builtin.is_known_data_type() and builtin.is_known_type_or_instruction() — now uses the expanded DATA_TYPES table from Phase 4 instead of inline string comparisons - Add attribute validation: warn on unknown per-variable attributes (SCL005). Valid attributes: ExternalWritable, ExternalVisible, ExternalAccessible, S7_SetPoint, S7_Optimized_Access - Allow optional semicolon after END_VAR parser.lua: - Add VAR DB_SPECIFIC to section detection and var_type classification - Make all block detection case-insensitive (FUNCTION_BLOCK, FUNCTION, ORGANIZATION_BLOCK, END_FUNCTION_BLOCK, etc.) - Make TYPE/END_TYPE, STRUCT/END_STRUCT, VAR/END_VAR case-insensitive - Fix VAR RETAIN vs VAR NON_RETAIN classification (NON_RETAIN was matching the RETAIN branch first) - Allow optional semicolon after END_VAR and END_STRUCT formatter.lua: - Add VAR DB_SPECIFIC and VAR_DB_SPECIFIC to BLOCKS and VAR_KEYWORDS - Fix get_keyword() to handle multi-word VAR keywords (VAR NON_RETAIN, VAR DB_SPECIFIC, VAR CONSTANT, VAR RETAIN) — normalizes spaces to underscores so the keyword lookup succeeds - Fix is_new_statement detection for space-form VAR keywords All 38 formatter tests pass. Parser and diagnostics load without errors.
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).