LSP¶
Related: Lexical Elements, Statements, POU Declarations
Document Information¶
| Property | Value |
|---|---|
| Version | 0.1.0 |
| Status | Draft |
| Last Updated | 2026-01-30 |
| Author | truST Contributors |
Table of Contents¶
- Overview
- Architecture
- Lexer Specification
- Parser Specification
- Semantic Analysis
- IDE Features
- LSP Protocol
- Runtime & Debugger
- Error Handling
- Performance Requirements
- Testing Strategy
- Current Implementation Status
1. Overview¶
1.1 Purpose¶
truST LSP is a Language Server Protocol implementation for IEC 61131-3 Structured Text (ST). It provides IDE features including diagnostics, completion, navigation, and refactoring for ST source code. The workspace also contains the ST runtime, the build of programs into program images (specification 44), and the debug adapter used for execution and testing.
1.2 Scope¶
This specification covers: - Lexical analysis of ST source code - Syntactic analysis and CST construction - Semantic analysis including type checking - IDE feature implementations - LSP protocol integration - Runtime execution of compiled program images - Debug adapter behavior and control protocols
1.3 Target Standard¶
Primary: IEC 61131-3 Edition 3.0 (2013)
With extensions for: - CODESYS v3.5 - Beckhoff TwinCAT 3 - Siemens TIA Portal (partial)
1.4 Design Goals¶
- Correctness - Accurately parse and analyze valid ST code
- Error Tolerance - Provide useful feedback even for invalid code
- Performance - Sub-100ms response times for interactive features
- Incrementality - Re-analyze only what changed
- Extensibility - Support vendor-specific extensions
2. Architecture¶
2.1 Crate Structure¶
trust-platform (workspace)
├── trust-syntax # Lexing and parsing
├── trust-hir # High-level IR and semantic analysis
├── trust-ide # IDE feature implementations
├── trust-lsp # LSP protocol layer
├── trust-runtime # Runtime execution engine + program build
└── trust-debug # Debug adapter (DAP)
2.2 Data Flow¶
Source Text
│
▼
┌─────────┐
│ Lexer │ → Token Stream
└─────────┘
│
▼
┌─────────┐
│ Parser │ → Concrete Syntax Tree (CST)
└─────────┘
│
▼
┌─────────┐
│ HIR │ → High-level IR + Symbol Table
└─────────┘
│
▼
┌─────────┐
│ IDE │ → Completions, Diagnostics, etc.
└─────────┘
│
▼
┌─────────┐
│ LSP │ → JSON-RPC Responses
└─────────┘
Runtime and debugger behavior are specified in:
- docs/specs/10-runtime-semantics.md
2.3 Key Dependencies¶
| Crate | Purpose | Version |
|---|---|---|
| logos | Lexer generation | 0.14 |
| rowan | Lossless syntax trees | 0.15 |
| salsa | Incremental query engine | 0.26 |
| tower-lsp | LSP framework | 0.20 |
2.4 Concurrency Model¶
- Single-threaded analysis (Salsa-backed source/parse/file symbols/
analyze/diagnostics/type_of) - Async I/O for LSP communication (tokio)
- Document store protected by RwLock
3. Lexer Specification¶
The LSP layer consumes the same lexer/token model defined in
01-lexical-elements.md. Keyword reservation, literals, identifiers, direct
addresses, and lexer diagnostics are owned by that language spec; this LSP spec
only defines how lexer-backed editor features surface them in diagnostics,
semantic tokens, completion, and navigation.
4. Parser Specification¶
The LSP layer consumes the concrete syntax and statement/declaration grammar
owned by 04-pou-declarations.md, 05-expressions.md, and
06-statements.md. This spec does not restate the grammar; it defines how the
editor-facing pipeline uses parser output for recovery, syntax diagnostics,
outline/navigation, and incremental document updates.
5. Semantic Analysis¶
Type rules, name resolution, assignability, and most semantic diagnostics are
owned by 02-data-types.md, 03-variables.md, 04-pou-declarations.md, and
09-semantic-rules.md. This LSP spec owns how that analysis is queried and
presented through IDE features such as diagnostics, hover, rename, and
workspace indexing.
6. IDE Features¶
6.1 Completion¶
6.1.1 Trigger Points¶
- After
.- member completion - After
:- type completion - After
(/ inside call arguments - parameter-name completions for formal calls (name :=/name =>) with direction-aware binding (IEC 61131-3 Ed.3, 6.6.1.4.2; Table 50/71) - After typed literal prefixes (
T#,DATE#,TOD#,DT#, etc.) - range-aware typed literal snippets with format hints (IEC 61131-3 Ed.3, 6.1.5; Tables 5-9) - Start of line - statement/keyword completion
- After
VARetc. - variable name suggestions
6.1.2 Completion Kinds¶
| Context | Suggestions |
|---|---|
After . on FB |
Properties, methods |
After . on STRUCT |
Fields |
After : |
Types in scope |
| Call arguments | Formal parameter names + in-scope expressions (IEC 61131-3 Ed.3, 6.6.1.4.2; Table 50/71) |
| Statement start | Keywords, variables, standard functions (IEC 61131-3 Ed.3, Tables 22-36) |
| Expression | Variables, literals, standard functions/FBs with IEC docs (IEC 61131-3 Ed.3, Tables 22-36, 43-46) |
6.2 Diagnostics¶
Diagnostics are delivered via both push (textDocument/publishDiagnostics) and pull (textDocument/diagnostic, workspace/diagnostic) APIs. Pull diagnostics return stable resultId values derived from content + diagnostic hashes, allowing unchanged responses when the client supplies the previous ID. On configuration/profile changes or workspace file updates, the server requests a refresh (workspace/diagnostic/refresh) when supported.
Warning diagnostics can be filtered via [diagnostics] configuration; rule packs can preconfigure defaults and severity overrides can promote warning codes to errors. Vendor profiles may adjust defaults to mirror tooling expectations (e.g., CASE/implicit conversion warnings per IEC 61131-3 Ed.3 §7.3.3.3.3 and §6.4.2).
Project configuration diagnostics are reported for trust-lsp.toml to flag library dependency issues (missing libraries or version mismatches).
External diagnostics can be merged from [diagnostics].external_paths JSON files, and optional fix payloads are exposed as quick-fix code actions.
6.2.1 Syntax Errors¶
- Missing tokens
- Unexpected tokens
- Unclosed blocks
6.2.2 Semantic Errors¶
- Undefined variable
- Type mismatch
- Duplicate declaration
- Invalid assignment target
- Missing return statement
- Task configuration errors (missing/invalid PRIORITY) and unknown task references in PROGRAM configs (IEC 61131-3 Ed.3 §6.2; §6.8.2; Table 62)
6.2.3 Warnings¶
- Unused variable
- Unused parameter
- Missing ELSE in CASE (IEC 61131-3 Ed.3, 7.3.3.3.3)
- Implicit type conversion (IEC 61131-3 Ed.3, 6.4.2)
- Non-determinism checks for time/date usage and direct I/O bindings (tooling lint; IEC 61131-3 Ed.3 §6.4.2 Table 10; §6.5.5 Table 16)
- Shared global access across tasks with writes (tooling lint; IEC 61131-3 Ed.3 §6.5.2.2 Tables 13–16; §6.2/§6.8.2 Table 62)
6.2.4 Diagnostic Explainability¶
When a diagnostic is mapped to an IEC reference, the LSP payload includes:
- codeDescription.href → file URL to the relevant docs/specs/*.md (when present in the workspace)
- data.explain → { iec: "...", spec: "docs/specs/..." }
Initial explainer coverage:
| Codes | IEC reference | Spec doc |
|---|---|---|
| E001–E003 | IEC 61131-3 Ed.3 §7.3 | docs/specs/06-statements.md |
| E101/E104/E105/W001/W002/W006 | IEC 61131-3 Ed.3 §6.5.2.2 | docs/specs/09-semantic-rules.md |
| E102 | IEC 61131-3 Ed.3 §6.2 | docs/specs/02-data-types.md |
| E103/E204/E205/E206/E207 | IEC 61131-3 Ed.3 §6.6.1 | docs/specs/04-pou-declarations.md |
| E106 | IEC 61131-3 Ed.3 §6.1.2 | docs/specs/01-lexical-elements.md |
| E201/E202/E203 | IEC 61131-3 Ed.3 §7.3.2 | docs/specs/05-expressions.md |
| E208 | IEC 61131-3 Ed.3 §6.6.7; Table 52 | docs/specs/04-pou-declarations.md |
| E301/E302 | IEC 61131-3 Ed.3 §7.3.1 | docs/specs/09-semantic-rules.md |
| E303 | IEC 61131-3 Ed.3 §6.4.4.5.1 | docs/specs/09-semantic-rules.md |
| E304 | IEC 61131-3 Ed.3 §6.4.2; §6.4.4.3–6.4.4.5 | docs/specs/02-data-types.md |
| W004 | IEC 61131-3 Ed.3 §7.3.3.3.3 | docs/specs/06-statements.md |
| W005 | IEC 61131-3 Ed.3 §6.4.2 | docs/specs/02-data-types.md |
| W008/W009 | Tooling quality lint (non-IEC) | docs/specs/09-semantic-rules.md |
| W010 | Tooling lint; host-clock reads (CURRENT_DT, the POUs that reach it, and the POUs that declare reads_host_clock) per specification 07 "Runtime Clock Sources", IEC 61131-3 Ed.3 §6.4.2 (Table 10) for the TIME/DATE family |
docs/specs/28-hir-warning-policy.md |
| W011 | Tooling lint; Direct variables per IEC 61131-3 Ed.3 §6.5.5 (Table 16) | docs/specs/09-semantic-rules.md |
| W012 | Tooling lint; shared global access across tasks (IEC 61131-3 Ed.3 §6.5.2.2 Tables 13–16; §6.2/§6.8.2 Table 62) | docs/specs/09-semantic-rules.md |
| W013/W014 | Tooling numeric-hazard lints (non-IEC) | docs/specs/09-semantic-rules.md |
| L001–L003 | Tooling config lint (non-IEC) | docs/specs/10-runtime-semantics.md |
For access-specifier violations reported under E202 (e.g., PRIVATE/PROTECTED/INTERNAL access),
the explainer is mapped to IEC 61131-3 Ed.3 §6.6.5 (Table 50) in docs/specs/09-semantic-rules.md.
Diagnostics without a mapping return only code + message until their IEC references are added.
6.2.5 Severity Levels and Warning Policy¶
| Severity | Description | Examples |
|---|---|---|
| Error | Must be fixed; compilation or validation cannot proceed | Type mismatch, undefined reference, invalid task binding |
| Warning | Likely bug or portability issue; build may still proceed | Unused variable, implicit conversion, unreachable code |
| Info | Supplemental context | vendor-profile hints, migration notes |
| Hint | Non-blocking editor guidance | optional quick-fix suggestions |
Recommended warning groups:
W003unreachable code after unconditional terminators or constant-false branchesW004missingELSEinCASEW005implicit conversionW008cyclomatic complexity quality lintW009unused POU quality lintW010/W011non-deterministic clock sources and direct-I/O usage.W010warns at a read of the host wall clock: a call ofCURRENT_DT(), a call of a function, method, function block or program whose body reaches one, directly or through the POUs it calls, or a call of a POU that declaresreads_host_clock(specification 07) — the message names the source the call reaches, and the declared POU's own file stays silent. Declarations of TIME/DATE/TOD/DT never warn, and neither does a source that cannot be traced, such as a body-less library stub without the declaration (redefined 5 November 2026, issue #250: sources, not declarations; declared stub sources added 6 November 2026, issue #264).W011concerns a direct%I/%Qaddress declared in reusable code (a function block, function, method or class), where it ties every use to one channel and to the I/O image's timing. A program's or configuration's I/O list is the normal place for addresses and does not warn (refined on 7 October 2026, when every entry of a plant's I/O list was warned of).W012shared global access across scheduled tasksW013/W014numeric hazard lints
Workspace warning policy is configured through trust-lsp.toml [diagnostics].
Profiles may override severities to mirror vendor expectations, but the
canonical code list and default severity guidance live in this spec.
6.2.6 Diagnostic Cancellation¶
Diagnostic cancellation must never be reported as a successful empty or partial diagnostic result:
- a cancelled push diagnostic collection publishes nothing;
- a cancelled
textDocument/diagnosticrequest returnsContentModified; - a cancelled
workspace/diagnosticrequest returnsContentModifiedfor the complete request and must not return the documents collected before cancellation as a successful partial report.
The server may reuse a completed result only when its content and diagnostic hashes still match. A stale semantic-analysis ticket is cancellation for this contract, including when a newer document or workspace request supersedes it.
6.3 Navigation¶
6.3.1 Go to Definition¶
- Variables → declaration
VAR_EXTERNALresolves to the matchingVAR_GLOBALdeclared in the associated program/configuration/resource scope (IEC 61131-3 Ed.3, §6.5.2.2; Tables 13–16, Table 47 feature 8a)- truST vendor-parity global access also resolves bare global names directly, and qualified names such as
GVL.sharedresolve against namespaced GVL entries recorded in runtime storage. - Types → type definition
- Methods → method definition
- Properties → property definition
6.3.2 Find References¶
- All usages of a symbol
- Include/exclude declaration
- Filter by read/write
6.3.3 Document Symbols¶
- Flat list of declarations
6.4 Refactoring¶
6.4.1 Rename¶
- All references updated (workspace-wide)
- Preview changes
- Namespace path moves via dotted rename or refactor action (updates namespace declarations,
USING, qualified names, and namespace-qualified field access; relocation across files moves the namespace block to a derived target file and removes the source file when empty; default target path mapsNamespace.Path→<workspace>/Namespace/Path.stunless an explicit URI is provided) (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66) - VS Code surfaces namespace relocation via
Structured Text: Move Namespace, prompting for the new path and optional target file (invokestrust-lsp.moveNamespace) (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66)
Rename Conflict Safety¶
Rename is atomic and fail-closed. Before returning edits, the server evaluates the complete candidate edit set against the merged project symbol table. It refuses the request when the replacement identifier:
- is invalid or reserved;
- names a different declaration in the target's declaring scope, including an imported or project-wide top-level declaration;
- collides with another field in the same structure or union; or
- would make any edited reference unresolved or resolve to a symbol other than the original target.
Conflict comparison is case-insensitive because IEC 61131-3 Ed.3 §6.1.2 defines identifier case as insignificant. A case-only rename of the same symbol is therefore allowed, but a spelling that differs only by case from another visible declaration is a collision. Refusal returns a named reason and no partial workspace edit.
6.4.2 Code Actions / Quick Fixes¶
- Create missing VAR declarations for undefined identifiers (IEC 61131-3 Ed.3, 6.5.3; Tables 13-14)
- Create missing TYPE definitions for undefined types (IEC 61131-3 Ed.3, 6.5.2; Table 11)
- Insert missing END_* blocks (IEC 61131-3 Ed.3, 7.3; Table 72)
- Insert missing RETURN in FUNCTION (IEC 61131-3 Ed.3, 7.3.3.3.2; Table 72)
- Convert formal ↔ positional call style (IEC 61131-3 Ed.3, 6.6.1.4.2; Table 50)
- Reorder mixed calls to positional-first argument order (IEC 61131-3 Ed.3, 6.6.1.4.2; Table 50)
- Move namespace path (refactor action invoking rename UI) (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66)
- Move namespace path (execute command; relocates declarations across files) (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66)
- Move namespace quick fix (VS Code lightbulb on
NAMESPACE/USINGlines; invokestrust-lsp.moveNamespacevia UI command) (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66) - Qualify ambiguous namespace references when multiple USING directives apply (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66)
- Fix VAR_OUTPUT binding operators / add missing OUT bindings (IEC 61131-3 Ed.3, 6.6.1.2.2; Table 71)
- Wrap implicit conversions using standard conversion functions (IEC 61131-3 Ed.3, Tables 22–27)
- Generate stub implementations for missing interface methods/properties from IMPLEMENTS clauses (IEC 61131-3 Ed.3, 6.6.5–6.6.6; Tables 50–51)
- Inline variable/constant with safety checks (const-expression analysis, no writes, cross-file constants when safe) (IEC 61131-3 Ed.3, 6.5.1–6.5.2; Tables 13–14)
- Extract method/property/function from a selection (method/property in CLASS/FB, function in POU body) with inferred VAR_INPUT/VAR_IN_OUT parameters; expression selections extract a FUNCTION returning the inferred expression type (IEC 61131-3 Ed.3, 6.6.5; Table 50 for methods/properties; 6.6.2.2; Table 19 for functions)
- Convert FUNCTION ↔ FUNCTION_BLOCK with safe call-site updates (supports qualified names and assignment/return expression sites; no recursive calls; FUNCTION→FB requires no existing VAR_OUTPUT when a return type is present; FB→FUNCTION requires a single VAR_OUTPUT and no type references/instances) (IEC 61131-3 Ed.3, 6.6.2.2; Table 19 and 6.6.3.2; Table 40)
- Remove unused variables/parameters
6.4.3 Future¶
- Change signature
6.5 Hover Information¶
Hover content includes: - Symbol signature + visibility/modifiers (IEC 61131-3 Ed.3, 6.6.5; Table 50) - Standard function/FB documentation (IEC 61131-3 Ed.3, Tables 22–36, 43–46) - Namespace/USING resolution details (IEC 61131-3 Ed.3, 6.6.4; Tables 64–66) - Typed literal guidance for TIME/DATE/TOD/DT prefixes (IEC 61131-3 Ed.3, 6.1.5; Tables 5–9) - Configuration/Resource/Task declarations show task scheduling inputs and program bindings (IEC 61131-3 Ed.3 §6.2; §6.8.2; Table 62)
motorSpeed : REAL
───────────────────
Variable (VAR_INPUT)
Declared in: FB_Motor
The target speed for the motor in RPM.
Range: 0.0 to 3000.0
6.6 Focused editor and command decisions¶
The following rules are truST language-tooling product contracts. They do not change IEC 61131-3 program semantics.
6.6.1 Client setting aliases¶
- Where a VS Code/LSP setting accepts both camelCase and snake_case spellings, camelCase is canonical and wins when both aliases are present.
- Alias lookup ignores values of the wrong JSON type and proceeds to the next reviewed alias rather than coercing them.
- The
stlspsection takes precedence over legacytrust-lspaliases. Runtime settings accept the documented nested and top-level forms without merging unrelated keys.
6.6.2 Binding catalogue command¶
trust-lsp.hmiBindingsrejects malformed argument shapes. A valid request returns the deterministic external binding catalogue of process-image and control symbols. ItsprocessImagelists the numeric plant candidates ascommands,feedbacksandmemory, and every addressed BOOL variable asbooleans, so that a declared state can name a bit.- HMI page validation is specified in
docs/specs/39-hmi-builder.md.
6.6.3 URI and virtual-document handling¶
- File URI conversion preserves spaces and fragments and normalizes supported platform-specific drive and extended-length forms without changing the underlying path identity.
- Virtual-document URIs are accepted for request processing when the client supplies their text; they are not rewritten as local file paths.
- Platform-gated URI tests that cannot execute on the current platform remain explicit nonmapping evidence rather than cross-platform proof.
6.6.4 Completion recovery and visibility¶
- Recovery completion in an incomplete statement keeps visible scope symbols.
- Member completion enforces declared visibility.
- Formal-parameter completion supports function and method calls and suppresses formals already used by the call.
- A cancelled completion request returns no completion payload. Cancellation is not reported as an empty successful analysis result.
6.6.5 Learner hints¶
- Learner diagnostics MAY add
Did you mean, conversion, and common syntax habit guidance when the primary diagnostic has a high-confidence correction. - Valid code and low-confidence candidates receive no learner-hint noise.
6.6.6 OpenOT editor projection¶
- OpenOT completion exposes only documented keys and values.
- OpenOT inlay hints identify the emitted record.
- The OpenOT logging code action is offered only for a declaration whose type supports that action.
6.6.7 Hover fallback and presentation¶
- When resolved runtime/type information is unavailable, hover falls back to the declaration's written type rather than omitting or inventing a type.
- Hover presents initializer and retention qualifiers from the declaration and keeps function-block member sections and parameter constants explicit.
6.6.8 Runtime inline-value merge¶
- Runtime instance fields merge into the matching local/namespace projection. Runtime values override the same resolved local identity; unrelated locals, constants, and instances remain present.
- CamelCase and snake_case runtime client settings follow the precedence rule in section 6.6.1.
6.6.9 Call-hierarchy file authority¶
- Call hierarchy includes incoming and outgoing calls only from the request's allowed project files. Dependency or excluded-file calls do not leak into a scoped result.
6.6.10 Namespace relocation transaction order¶
- Namespace relocation applies create operations first, then content edits, then deletion of an emptied source file.
- Failure before the delete phase leaves the original source available; no command may delete first and attempt recovery afterward.
6.6.11 Browser and WebAssembly analysis projection¶
The browser analysis engine owns an in-memory document set. A successful replacement makes exactly the supplied URI and text pairs the next analysis snapshot. Its status reports that snapshot's document count and URI identities. Repeated full-set replacements must not retain diagnostics produced only by superseded text, including when several documents change in one replacement.
For the same accepted document set and request, the browser projection of diagnostics, hover, and completion must preserve the corresponding native analysis result. This is a result-projection requirement for the reviewed features and fixtures; it does not claim identical performance, allocation, or every unreviewed native capability.
Completion preserves visible program variables for partial statement prefixes and declared structure members after member access. Function-block hover preserves declared input and output types instead of replacing resolved types with unknown placeholders.
Browser navigation and refactoring treat supplied source keys as opaque virtual
document identities. Plain names such as program.st are not rewritten into
filesystem URIs. In the reviewed cases, definition preserves its asserted
cross-document target identity, references and rename preserve the asserted URI
membership, and document highlight preserves the asserted occurrence count.
These feature requests also accept the reviewed cursor positions at an
identifier boundary or immediately adjacent punctuation. This does not claim
exact ranges, highlight kinds, edit contents, conflict handling, edit
application, or atomicity beyond the asserted results.
The WebAssembly JSON adapter is a serialization boundary over the same browser engine:
- malformed document JSON returns an explicit error rather than a successful empty snapshot;
- accepted document JSON returns a parseable replacement result;
- status JSON returns the current document count and URI identities; and
- diagnostic JSON returns the current diagnostics for the requested URI.
These are truST product and host-adapter contracts. They do not define IEC
program semantics and are not IEC 61131-3 deviations. The reviewed tests invoke
the native Rust facades; they do not by themselves prove a wasm32,
wasm-bindgen, JavaScript, or rendered-browser integration lane.
7. LSP Protocol¶
7.1 Supported Capabilities¶
| Capability | Method | Status | Notes |
|---|---|---|---|
| Text Sync | textDocument/didOpen, etc. |
✅ | Incremental sync with full-change fallback |
| Diagnostics | textDocument/publishDiagnostics |
✅ | Parse + semantic diagnostics (undefined names, type mismatch, invalid assignments) |
| Pull Diagnostics | textDocument/diagnostic |
✅ | Per-file result IDs; unchanged when previousResultId matches |
| Workspace Diagnostics | workspace/diagnostic |
✅ | Full/unchanged reports per document across indexed workspace |
| Diagnostics Refresh | workspace/diagnostic/refresh |
✅ | Server requests refresh on config/profile or workspace changes (client-supported) |
| Completion | textDocument/completion |
✅ | Scope-aware + member access + parameter-name completion + standard docs |
| Hover | textDocument/hover |
✅ | Shows type + qualifiers |
| Signature Help | textDocument/signatureHelp |
✅ | Call signatures with active parameter |
| Definition | textDocument/definition |
✅ | Project-wide (workspace indexed; file watching updates) |
| Declaration | textDocument/declaration |
✅ | Same target as definition |
| Type Definition | textDocument/typeDefinition |
✅ | Type/alias definition lookup |
| Implementation | textDocument/implementation |
✅ | Interface implementers (project-wide) |
| References | textDocument/references |
✅ | Symbol-aware (workspace indexed; no text fallback); work-done progress + partial results when client provides tokens |
| Document Highlight | textDocument/documentHighlight |
✅ | Highlight reads/writes in current document |
| Symbols | textDocument/documentSymbol |
✅ | Flat list |
| Workspace Symbols | workspace/symbol |
✅ | Multi-root symbol federation with per-root priority/visibility; work-done progress + partial results when client provides tokens |
| File Rename | workspace/willRenameFiles |
✅ | Renames single top-level POU/namespace when file stem changes; updates references and USING directives for that namespace (IEC 61131-3 Ed.3, 6.1.2; 6.6.4; Tables 64-66) |
| Rename | textDocument/rename |
✅ | Symbol-aware; workspace edits; renames the declaring file when renaming the single primary POU whose identifier matches the file stem (IEC 61131-3 Ed.3, 6.1.2) |
| Semantic Tokens | textDocument/semanticTokens |
✅ | Full + range + delta; classified by symbol kind/modifiers |
| Semantic Tokens Refresh | workspace/semanticTokens/refresh |
✅ | Server requests refresh on config/profile changes (client-supported) |
| Folding Range | textDocument/foldingRange |
✅ | CST-based region folding |
| Selection Range | textDocument/selectionRange |
✅ | CST-based hierarchical selection ranges |
| Linked Editing | textDocument/linkedEditingRange |
✅ | Identifier-linked ranges in document (IEC 61131-3 Ed.3, 6.1 identifiers) |
| Document Link | textDocument/documentLink |
✅ | Links for USING directives and trust-lsp.toml path entries (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66) |
| Inlay Hints | textDocument/inlayHint |
✅ | Parameter-name hints for positional calls (IEC 61131-3 Ed.3, 6.6.1.2.2; Table 71) |
| Inline Values | textDocument/inlineValue |
✅ | Constant/enum references show initializer text; runtime values surfaced via debug control for locals/globals/retain when configured (IEC 61131-3 Ed.3, 6.5.1–6.5.2; Tables 13–14) |
| Code Lens | textDocument/codeLens |
✅ | Reference count lenses for POU declarations |
| Call Hierarchy | textDocument/prepareCallHierarchy |
✅ | Incoming/outgoing call graph for POU declarations |
| Type Hierarchy | textDocument/prepareTypeHierarchy |
✅ | Class/FB/interface supertypes + subtypes (IEC 61131-3 Ed.3, 6.6.5) |
| Formatting | textDocument/formatting |
✅ | Indentation + spacing + alignment + wrapping (configurable) |
| Range/On-Type Formatting | textDocument/rangeFormatting, textDocument/onTypeFormatting |
✅ | Line-based formatting using document formatter |
| Configuration | workspace/didChangeConfiguration |
✅ | Settings stored (formatting/indexing); project config file is separate |
| Code Actions | textDocument/codeAction |
✅ | Quick fixes for unused symbols, missing END_* / RETURN, call style conversion, namespace disambiguation, implicit conversion, etc. |
| Execute Command | workspace/executeCommand |
✅ | trust-lsp.moveNamespace for namespace relocation across files (IEC 61131-3 Ed.3, 6.6.4; Tables 64-66); trust-lsp.projectInfo surfaces build flags, targets, and library dependency graph; the five Devices & Connections commands trust-lsp.devicesView, trust-lsp.devicesAct, trust-lsp.devicesBrowse, trust-lsp.devicesSearch and trust-lsp.devicesClose reach the panel engine (specification 25 §5.15) |
7.1.1 Position Encoding and Line Boundaries¶
truST supports the mandatory LSP UTF-16 position encoding and advertises
utf-16 in the initialize result. Incoming and outgoing positions, ranges,
text edits, and semantic-token starts and lengths count UTF-16 code units. A
supplementary-plane scalar therefore contributes two character units.
The line index recognizes all LSP 3.17 line endings: LF (\n), CRLF
(\r\n), and bare CR (\r). Each sequence advances exactly one line and its
terminator bytes are not addressable as positions. A character value beyond
the line length clamps to the line end, as required by the
LSP 3.17 Position contract.
7.1.2 Progressive Results and Interactive Latency¶
When a references or workspace-symbol request supplies a partial-result token,
truST sends each non-empty result chunk through $/progress using that exact
token. The final request response is empty after streaming so a client cannot
count the same locations or symbols twice. Without a partial-result token, the
complete result remains in the final response.
Runtime-assisted inline values are an optional interactive enhancement, not a reason to block the editor. Connect, read, and write operations against the configured runtime control endpoint use a 250 ms I/O bound. An unavailable, silent, or malformed endpoint produces no runtime-derived inline values and returns within that bound; static inline values remain independently available. This timeout behavior is tooling policy, not IEC program semantics.
The endpoint scheme is exactly tcp://, plus unix:// on Unix platforms,
with a nonempty address/path after configuration-level trimming. Each
newline-delimited JSON request starts at ID 1 and increments monotonically,
uses type = "debug.scopes" or type = "debug.variables", carries the exact
frame or variables reference in params, and includes auth only when a
nonblank configured token exists. An empty response, malformed JSON, missing
required response fields, ok = false, or absent result fails the current
runtime snapshot without exposing partial data.
The scopes result must contain one unambiguous, nonzero reference for each
advertised known scope. Known names are case-insensitive locals, globals,
retain, and instances; unknown scopes are ignored. Each requested
variables result must contain a valid array of string name/value pairs. Names
are trimmed, blank names are discarded, and duplicate names retain their first
value. If any advertised locals/globals/retain scope cannot be fetched, the
entire runtime snapshot is rejected rather than mixing values from different
runtime observations.
Instance selection is deterministic. Owner hints are considered in caller
order, case-insensitively: an exact qualified type#instance prefix wins,
then an exact type name, then a unique unqualified base-name match. Ambiguous
base-name matches select nothing. If no hint selects and exactly one instance
exists, that instance is used; multiple unmatched instances are ignored.
Selected instance variables supplement locals, while explicit locals win on
duplicate names. Instance or optional-scope absence is not itself an error.
Authentication tokens and returned variable values are never written to
diagnostic or debug logs.
7.2 Document Synchronization¶
- Incremental sync using
TextDocumentContentChangeEventranges. - Full-document replacement is supported when the change range is omitted.
- An incremental range whose start or end line is outside the current document,
or whose start follows its end, is rejected without changing the last valid
buffer. Because
didChangeis a notification, the server logs the reason and sendswindow/showMessagewith a full-resynchronization recovery action instead of returning a request error. - Range and on-type formatting use the same LF, CRLF, bare-CR, and UTF-16 line model as synchronization and navigation, and preserve the document's line ending in emitted edits.
7.2.1 Document Close and Durable Project Truth¶
textDocument/didClose discards the server's unsaved buffer for the closed
URI. Closing a document never writes that buffer to disk and never treats its
contents as durable project state.
- When the URI resolves to a readable file, the server reloads the file from disk, marks the document closed, and uses those bytes as project truth.
- When the URI is not a file URI or its file cannot be read, the server removes the document from the project index.
- Closing invalidates semantic, semantic-token, and diagnostic caches that could retain the discarded contents. Dependent files are recomputed against the reloaded or removed durable source. It also cancels semantic work started from the discarded buffer; cache writers verify that request generation while holding the cache lock, so a late result cannot repopulate an evicted cache.
- For push diagnostics, the server publishes an empty diagnostic list only for the closed URI. It does not publish an empty-success result for dependent files. Pull-diagnostic clients receive results recomputed from durable project truth on their next request.
This lifecycle is an LSP/project-state contract. It does not define IEC program execution or an IEC 61131-3 deviation.
7.3 Semantic Token Types¶
| Token Type | Usage |
|---|---|
| keyword | All ST keywords |
| type | Type names |
| variable | Variable names |
| property | Property names |
| method | Method names |
| function | Function names |
| parameter | Parameter names |
| number | Numeric literals |
| string | String literals |
| comment | Comments |
| operator | Operators |
7.4 Semantic Token Modifiers¶
| Modifier | Usage |
|---|---|
| declaration | At declaration site |
| definition | At definition site |
| readonly | CONSTANT variables |
| static | VAR_STAT variables |
| modification | Write to variable |
7.5 Formatting¶
- Indentation and token-based spacing normalization (operators/separators).
- VAR block
:alignment across declarations. - Assignment alignment for
:=and=>within aligned blocks (range formatting expands to align pasted statement lists). - Keyword casing (upper/lower/preserve), spacing style (spaced/compact), end keyword indentation (aligned/indented), and max line length are configurable;
vendor_profilepresets default indent width, spacing, and end keyword style for common IDEs. - Block comment lines are left unchanged; line comments and pragma lines preserve inline spacing.
- String literal and pragma lines are excluded from assignment alignment and wrapping to preserve lexical content (IEC 61131-3 Ed.3, 6.1; Tables 4–7).
- Line endings are preserved (LF vs CRLF).
- Line-wrapping at commas honors
maxLineLengthand avoids comment/pragma/string lines (IEC 61131-3 Ed.3, 6.1; Tables 4–7). - Range formatting expands to the nearest syntactic block (e.g., VAR blocks, IF/CASE loops, POU/method/property bodies) to avoid partial-block drift.
- VAR alignment respects manual grouping: blank lines or comment/pragma lines split alignment groups to preserve intentional spacing and comment anchors.
- Formatting config keys:
indentWidth,insertSpaces,keywordCase,spacingStyle,endKeywordStyle,alignVarDecls,alignAssignments,maxLineLength. - Vendor preset defaults (overrideable via config):
codesys/beckhoff/twincat/mitsubishi/gxworks3use 4-space indents with spaced operators;siemensuses 2-space indents with compact operator spacing; all alignEND_*keywords by default.
7.6 Project Configuration & Workspace Indexing¶
- Per-root project config file:
trust-lsp.toml,.trust-lsp.toml, ortrustlsp.toml. [project]supportsinclude_paths(the project's own source folders),library_paths(library folders, indexed like[[libraries]]entries named after their folder),vendor_profile(language rules + formatting presets), andstdlibselection.vendor_profileselects the language rules of the whole analysis (diagnostics, hover, completion,trust-lsp check), and the runtime build, the debugger and the runtime's web IDE take the same rules from the project'svendor_profile, with the conversion rules it selects (docs/specs/07-standard-functions.md section 2.10):iec,siemens,mitsubishi,gxworks3, an empty name or none: IEC 61131-3 as truST reads it.codesys,beckhoff,twincat: the IEC rules with CODESYS's departures, each recorded indocs/IEC_DEVIATIONS.md: function block members public by default (DEV-028), a function block's or program's body assigning its own inputs (DEV-029), a program reached by its name (Program.member,Program.action(),Program();, DEV-030), references dereferenced without^(DEV-031), and a named actual without a value leaving the input unbound or discarding the output (DEV-032).- A misspelled profile keeps the IEC rules in the editor and reports
C006. - With several workspace folders the analysis follows the
vendor_profileof the highest-priority folder's configuration ([workspace] priority). - Vendor extensions that change no valid IEC program are accepted under every profile: textual actions called by name,
REF=, theREFERENCE TOspelling,EnumType.Member, enum-typed subscripts,RETAIN PERSISTENTandTYPE … EXTENDSon structures. stdlibprofiles:full(default),iec(IEC standard functions/FBs only; Tables 22–36, 43–46),none(no standard library completions/hover), or an explicit allow-list array.- When
vendor_profileis set and no explicit stdlib allow-list/profile is provided, the server defaults to the IEC profile for completions/hover. [[libraries]]entries includename,path, and optionalversionfor external library indexing.[dependencies]supports local and git package references:- local:
Name = "path"orName = { path = "...", version? = "..." } - git:
Name = { git = "<url-or-local-repo>", rev? = "...", tag? = "...", branch? = "...", version? = "..." } - Intended usage split:
[dependencies]is for reusable truST ST packages that participate in source resolution,trust-runtime build, andtrust-dev test --project.[[libraries]]is for external/indexed library trees, stub packs, and attached vendor docs used for compatibility/indexing.- Dependency pinning/lock behavior:
rev/tag/branchpin git dependencies explicitly.build.dependencies_locked = truerequires explicit pinning or a matching lock entry.- Resolver snapshots pinned sources to
build.dependency_lockfile(defaulttrust-lsp.lock) for reproducible resolution. build.dependencies_offline = truedisables clone/fetch and resolves from local cache + lock only.- Basic supply-chain trust policy is configurable via
[dependency_policy]: allowed_git_hosts = ["example.com"]allow-list (empty = any host).allow_http(default false),allow_ssh(default false).
Dependency graph, trust, and lock integrity¶
Dependency identity is nonblank, trimmed, and case-insensitive throughout the
complete transitive graph. Declaration order does not affect resolution or
diagnostics. A manifest entry sets exactly one nonblank source: a path string
or path table entry, or a nonblank git URL. Path entries cannot carry git
selectors. Git entries may carry at most one nonblank rev, tag, or
branch. Optional versions are trimmed, nonblank exact package-version
constraints; truST does not silently coerce version syntax.
Local paths resolve relative to the manifest that owns the entry and are
canonicalized before identity comparison, lock recording, or indexing. The
resolved target must be a readable directory. A missing dependency manifest
is permitted for a source-only package and yields an unspecified version; a
present unreadable or malformed manifest is L001 and the invalid package is
not exposed as a resolved library.
Resolution is transitive and deterministic. Each canonical
case-insensitive dependency identity resolves to exactly one canonical source
and one compatible version requirement. A second declaration for the same
identity with another source is L003; a conflicting required/resolved version
is L002. Self-dependencies and longer cycles are L004, with the cycle path
reported. A cycle, conflict, malformed entry, missing source, or invalid
manifest never produces a partially authoritative library entry for the
affected identity. Independent valid graph components remain available and
retain their own diagnostics.
trust-lsp.lock schema version 1 records every resolved dependency in
case-insensitive deterministic name order. Path entries record the canonical
path. Git entries record the exact URL and full resolved commit. Locked mode
requires a matching entry for every unpinned source, including local path
identity; a missing entry, unsupported lock version, source-kind mismatch,
canonical-path mismatch, URL mismatch, malformed lock, or empty revision is
L006 and blocks that dependency. Explicit git pins must resolve to the same
commit as a matching locked entry when locked mode is enabled.
Unlocked resolution writes a new lock only after the entire graph resolves
without issues. Publication is atomic: create parent directories, write and
flush a sibling temporary file, then replace the destination. A failed encode,
write, flush, or rename preserves the previous lock byte-for-byte and emits
L006. Locked and offline resolution never rewrites the lock. A custom
build.dependency_lockfile path is resolved relative to the project root.
Offline mode performs no clone, fetch, checkout that changes the cached
worktree, or other network access. It requires the named cache plus a matching
full commit from the lock or explicit revision and reports L007 when the
source or revision is unavailable. Normal online mode may populate or refresh
the deterministic .trust-lsp/deps/git/<sanitized-name>-<url-hash> cache.
Cache directory names are stable across processes and do not contain path
separators or credentials.
Local paths and file:// git URLs are local sources. HTTPS is enabled by
default. Plain HTTP and SSH/SCP syntax require their explicit policy flags.
When allowed_git_hosts is nonempty, host comparison is
case-insensitive and accepts the exact host or its subdomains only; suffix
lookalikes are rejected. User information and ports do not change the host
identity, bracketed IPv6 hosts are parsed as one host, credentials are never
copied into diagnostics or cache names, and unknown URL schemes are L005.
Trust rejection happens before clone, fetch, cache creation, or lock mutation.
The stable resolver codes are:
L001: missing/unreadable source, manifest, or source operation;L002: package-version mismatch;L003: conflicting source declarations for one dependency identity;L004: dependency cycle;L005: malformed dependency entry or rejected source policy;L006: lock schema, identity, content, or publication failure; and-
L007: offline cache or revision unavailable. -
[[libraries]]can declaredependencies(array of{ name, version? }) to model library graphs; missing dependencies or version mismatches are reported as config diagnostics. - Library/dependency graphs report missing references (L001), version mismatches (L002), conflicting declarations (L003), and dependency cycles (L004).
- Library files are API surface for the unused-symbol warnings (specification 28 §1). A project's library folders are its
[[libraries]]folders, its[project] library_paths, and its resolved[dependencies]packages, transitive ones included; its own source folders are itsinclude_paths, or its root when none are set. For each file, the deepest folder that holds it among the project's own source folders and its library folders decides. When that folder is a library folder, the file is library surface: it never receives W001, W002, or W009, in the editor and intrust-lsp checkalike, while every other diagnostic stays on. A library file outside the workspace folder follows the project whose library it is (§7.6.4). An own source folder wins over an equally deep library folder, so a library entry equal to an own source folder ([[libraries]] path = "."in a project withoutinclude_paths, or a library at an include path) exempts nothing. A library folder that holds the project, asPlant = { path = "../.." }holds a Plant example, exempts the library's other files but not the project's own. Folders and files are compared component by component after resolving symbolic links, junctions, and short names, a missing file or folder through its deepest existing folder, so every spelling of one folder is the same folder. No library entry is refused for where its folder lies. A library is judged in its own project, where its files are that project's own sources and every warning is on. [[libraries]]can declaredocs(array of markdown files) to attach vendor library documentation to hover/completion. Each file uses# SymbolNameheadings followed by doc text.[workspace]controls multi-root federation:priorityorders root results for workspace symbol search, andvisibility(public,private,hidden) filters which roots participate when querying (private roots only appear for non-empty queries) (tooling behavior, non-IEC).[build]exposes project compile flags (flags),defines, and optionaltarget/profiledefaults.debug(a Boolean,truewhen absent) chooses the native program image thattrust-runtime build --targetcompiles:falsebuilds a release image without debug hooks (specification 44 §13.2);profiledoes not choose the image. The language server reads no other meaning intodebug.[signing]keynames the file of the Ed25519 key, in PKCS#8 PEM and relative to the project folder, that the tools sign the project's program images with; the file must exist. Without it they sign with the user's own key in truST's configuration folder (specification 44 §5.11). The language server reads no meaning into[signing].[[targets]]describes target profiles (name,profile,flags,defines) surfaced to LSP clients for toolchain selection.[indexing]budgets (max_files,max_ms) bound large workspace indexing.[indexing]cache options:cache(default true) enables persistent index caching across sessions;cache_diroverrides the cache location. Cache reuse checks file metadata and stored content hashes.[indexing]memory budget controls:memory_budget_mbcaps closed-document index memory (MB) andevict_to_percentdefines the LRU eviction target; evicted documents are reloaded on demand when accessed.[indexing]adaptive throttling:throttle_idle_ms,throttle_active_ms,throttle_max_ms, andthrottle_active_window_mspace background indexing based on recent editor activity and observed per-file work.[runtime]supportscontrol_endpointand optionalcontrol_auth_tokenfor debug-assisted inline values; a repositorycontrol_endpointis contacted only with the repositorycontrol_auth_token(never the editor's token), and the editor'scontrolAuthTokensetting is sent only to the editor's owncontrolEndpointsetting.[diagnostics]toggles warning categories (warn_unused,warn_unreachable,warn_missing_else,warn_implicit_conversion,warn_shadowed,warn_deprecated,warn_complexity,warn_nondeterminism,warn_numeric_hazards) for vendor-dialect alignment (IEC 61131-3 Ed.3 §6.4.2; §7.3.3.3.3). Cyclomatic complexity warnings (W008) use a default threshold of 15; unused warnings (W001/W002/W009) cover variables, parameters, and top-level POUs; numeric hazard warnings (W013/W014) cover floating-point equality and literal zero divisors.[diagnostics].rule_packpresets safety-focused defaults (e.g.,iec-safety,siemens-safety,codesys-safety,beckhoff-safety,twincat-safety,mitsubishi-safety,gxworks3-safety); explicitwarn_*keys override pack defaults.[diagnostics].severity_overridescan promote specific warning codes to error severity (W004 missing ELSE per IEC 61131-3 Ed.3 §7.3.3.3.3; W005 implicit conversion per §6.4.2; W010 TIME/DATE nondeterminism per §6.4.2; W011 direct variables per §6.5.5; W014 literal division/modulo by zero guard).[diagnostics].external_pathslists JSON diagnostics payloads from external linters (optional per-diagnostic fix data yields quick-fix actions).- Vendor diagnostic defaults:
siemensdisables Missing ELSE (W004) and implicit conversion (W005);codesys,beckhoff,twincat,mitsubishi, andgxworks3keep all warning categories enabled unless overridden in[diagnostics]. [telemetry](opt-in) records aggregated feature usage + latency to JSONL (enabled,path,flush_every); payloads include event names and durations only (tooling behavior, non-IEC).- Indexing progress is reported via
window/workDoneProgresswhen supported by the client. - Workspace indexing runs in the background; adaptive throttling yields between files to keep interactive edits responsive (tooling behavior, non-IEC).
- Stdlib selection currently filters standard function/FB docs and completions (IEC 61131-3 Ed.3, Tables 22–36, 43–46).
Configuration normalization and bounded values¶
Configuration diagnostics use a stable C namespace:
C001: the selected configuration file cannot be read or parsed;C002: an unknown standard-library profile fell back tofull;C003: a configured numeric bound was outside its documented domain, or the throttle tuple was incoherent, and normalization fell back to a safe documented value or tuple;C004: an unknown workspace visibility fell back topublic;C005: a diagnostic severity override has a blank code or unknown severity and was ignored; andC006: thevendor_profileis not one ofbeckhoff,codesys,gxworks3,iec,mitsubishi,siemensortwincat. The server keeps its defaults for it, while the tools that run the project (trust-runtime,trust-dev test, debug sessions,trust-harness --project) refuse it because it selects the conversion rules (docs/specs/07-standard-functions.md section 2.10).
Configuration filename precedence is trust-lsp.toml, then
.trust-lsp.toml, then trustlsp.toml. An absent file produces the documented
defaults. A present unreadable or malformed file is not partially applied:
the server retains the selected config path, uses the complete safe default
model, and reports the parse/read failure through the configuration diagnostic
surface.
Every textual scalar is trimmed. Empty optional strings become absent.
Path lists discard empty entries, resolve relative entries against the project
root, preserve absolute entries, and deduplicate canonical identities while
retaining first-declaration order. The same normalization applies to include,
library, documentation, external-diagnostic, cache, telemetry, and lock paths.
indexing_roots uses explicit include roots instead of the workspace root,
then adds resolved libraries once; without includes it uses the workspace root.
The standard-library profiles are canonical lowercase full, iec, and
none. An explicit allow list is trimmed, removes blank entries, and
deduplicates names case-insensitively in first-declaration order. none
produces an explicit empty allow list. An unknown named profile uses full
and emits a configuration diagnostic rather than silently disabling symbols.
When no explicit selection exists, the vendor fallback described above is
applied by the feature filter.
Indexing optional budgets, memory budgets, and telemetry flush_every are
positive when present. Invalid zero values use their documented defaults or
become unbounded as appropriate and emit a configuration diagnostic.
evict_to_percent is normalized to 1..=100. Throttle delays satisfy
idle <= active <= max; the activity window is positive. Invalid
relations use the corresponding default tuple atomically so one bad field
cannot create an incoherent mix. An out-of-range eviction percentage or an
incoherent throttle tuple emits C003 so this replacement is visible to the
user. Disabling the cache makes
index_cache_dir() return none even when a path is configured.
Workspace visibility is case-insensitive public, private, or hidden;
unknown text falls back to public with a configuration diagnostic. Private
roots answer only nonempty queries and hidden roots answer none. Target names
are nonblank and case-insensitively unique. Build target/profile, target
profile, flags, and defines are trimmed; blank values are removed and list
values deduplicated in first-declaration order.
Diagnostic rule-pack names and override codes are normalized
case-insensitively. Safety packs establish their complete baseline first,
vendor-specific pack adjustments apply next, and explicit warn_* booleans
and severity overrides apply last. Override aliases are error|err,
warning|warn, info|information, and hint. Blank codes or unknown
severities are ignored with a configuration diagnostic; accepted codes are
stored uppercase, with exactly one severity stored per normalized code. When
multiple raw override keys normalize to the same code, entries within the
alias and trimmed-canonical groups are applied in raw-key lexical order, and
the trimmed canonical uppercase group is applied after all aliases.
Consequently, the lexical-last alias wins an alias-only collision, while a
trimmed canonical uppercase key always wins when one is present. This is a
truST LSP configuration rule and does not change IEC program semantics.
Telemetry is disabled by default. Enabling it without a path selects
.trust-lsp/telemetry.jsonl; an explicit path is retained even while disabled
so later enablement is stable. Runtime control endpoint and token values are
trimmed, with blank values treated as absent. Configuration orientation and
debug output expose only whether a token exists, never its value.
7.6.1 Workspace coordination and helper projections¶
The first-index-pass signal is a one-way latch. A waiter that subscribes before completion remains blocked until the pass is marked complete or the bounded deadlock timeout expires. Marking completion is idempotent, and a waiter that subscribes after completion returns immediately.
Background workspace requests acquire the configured background permit before their work begins, so the default single-permit lane serializes them. Closing the limiter disables admission blocking rather than preventing the already supported background operation from running.
Library documentation is cached by workspace configuration. Repeated reads under an unchanged configuration reuse the same parsed map. Reapplying workspace configuration invalidates the entry and the next read observes the current documentation contents.
The command wrapper and its ServerContext implementation project identical
projectInfo results for the same workspace state. IDE source helpers preserve
the following deterministic projections used by commands and refactors:
- a variable declaration's declared type is the trimmed text after
:and before an initializer or terminator, including aggregate type text; - exact symbol ranges resolve to their owning variable declaration and carry that declared type;
- source text selected by a valid range is trimmed; and
- extending a range to its line end preserves its start and includes the trailing LF or CRLF when one is present.
These are truST tooling contracts. They do not add IEC language semantics.
7.6.2 UTF-16 text positions and incremental changes¶
Document positions use zero-based lines and UTF-16 code-unit columns, as required by the LSP default position encoding. The line index recognizes LF, CRLF, and lone CR as line terminators, excludes terminator bytes from line text, and represents a trailing terminator with a final empty line. An offset inside a multi-byte scalar moves to that scalar's start. An offset inside any line terminator maps to the preceding line end; an offset beyond the document maps to end-of-file. Supplementary Unicode scalars count as two UTF-16 units.
Position-to-offset conversion rejects a line outside the document. A column inside a supplementary scalar selects the scalar's start, and a column beyond the line defaults to the line end, matching the LSP position rule. Conversions at every scalar boundary and at end-of-file round-trip. UTF-16 length calculation clamps byte inputs to scalar boundaries and returns zero for an empty or reversed interval.
Incremental changes are applied in declaration order, and each range is interpreted against the text produced by all preceding changes. A range-less change replaces the complete document and can be followed by another incremental change. Insertion, deletion, single-line replacement, and multi-line replacement preserve all untouched bytes and line-ending style. An out-of-range start or end line and a start after the normalized end fail closed with a stable typed error. The caller receives no partially updated document when any change in a batch is invalid.
This is the LSP text synchronization contract. It does not alter IEC source semantics.
7.6.3 Persisted and external-input integrity¶
The persistent index cache is an optimization, never semantic authority.
index.json has an explicit schema version. An absent, unreadable, malformed,
or unsupported-version file loads as an empty current-version cache. A cache
hit requires the current file to exist, be readable UTF-8, and match the
recorded size, modification time, and content hash; metadata equality alone is
insufficient. Updating unchanged content refreshes its disk signature without
duplicating the entry. Removal and retention operate on normalized path
identities. Saving publishes one complete JSON document and must not destroy a
previous valid cache if serialization or replacement fails. Cache failure may
cost performance but cannot change diagnostics, symbols, or language results.
Library documentation files use CommonMark-style ATX headings (# through
######, with required whitespace) as symbol boundaries. Heading text is
trimmed and indexed case-insensitively; prose before the first valid heading,
blank headings, and headings with an empty body do not create entries. Body
line order and paragraph breaks are preserved with surrounding blank space
trimmed. A later declaration of the same symbol replaces the earlier one,
including declarations from later configured files. Missing, unreadable, or
non-UTF-8 documentation files contribute no entries and do not erase entries
already read from other files. Lookup is ASCII-case-insensitive.
Library graph identities and dependency references are ASCII-case-insensitive
after configuration normalization. Graph node order follows configuration
order. An absent dependency emits L001; an explicit version requirement with
no exact configured match emits L002; conflicting declarations of one
library identity emit one deterministic L003; and each distinct dependency
cycle, including a self-cycle, emits one deterministic canonical L004.
Unversioned requirements accept any configured version. Duplicate dependency
edges and duplicate declarations do not duplicate issues. Issue ordering is
stable: conflicts, missing/version issues in configuration order, then
canonical cycles.
Each configured external-diagnostics JSON file is one atomic input document.
It is either a top-level list or an object with a diagnostics list. An
unreadable, malformed, or schema-invalid document contributes nothing rather
than partially applying entries. Each entry must select a target using uri
when present, otherwise a path resolved against the project root; invalid URIs
and entries for another document are ignored. LSP ranges are zero-based and
preserved exactly. String severity values are trimmed before
ASCII-case-insensitive matching and accept
error|warning|info|information|hint. Numeric severity values accept LSP
values 1..=4. An absent or invalid severity defaults to warning. Source
defaults to external. String codes remain strings. Optional fix data
preserves its title, replacement text, and optional replacement range. File and
entry declaration order determine diagnostic order.
Telemetry is disabled unless explicitly opted in and records only aggregate
event identities and duration statistics. The event vocabulary is stable and
contains no source text, URI, workspace path, symbol, diagnostic message,
runtime endpoint, authentication token, or user identifier. Durations are
whole milliseconds, clamped to u64, with saturating count and total and exact
minimum/maximum. Events aggregate by identity until the positive flush
threshold is reached or an explicit flush, disable, sink-path change, or
threshold change occurs. Each successful flush appends exactly one JSONL
record and clears the aggregate. Empty flushes do not create records. A failed
open, directory creation, or write retains the aggregate for retry rather than
silently discarding it. Switching or disabling a sink first attempts to flush
the old aggregate; a failed flush remains observable and must not be replaced
as though publication succeeded.
These cache, documentation, graph, external-diagnostic, and telemetry rules are truST product contracts outside IEC 61131-3. They do not define PLC language semantics.
7.6.4 Server-state lifecycle and cache contract¶
A newly constructed server state has no documents, workspace folders, workspace
configurations, library-document entries, semantic tokens, diagnostics, or
call-hierarchy results. Client capability flags are false, configuration is
JSON null, activity age is the never-recorded sentinel, and the document and
semantic-request generations begin at one. Default and new establish the
same state. A Document records UTF-8 content size in bytes, not Unicode scalar
count.
Workspace folders and client configuration are stored as owned snapshots; mutating a returned value cannot alter server state. Capability flags are independent. Pull diagnostics are selected only when the client supports both pull diagnostics and diagnostic refresh. Workspace configuration has one entry per root, with replacement on the same root. The primary configuration is the highest numeric workspace priority. URI lookup chooses the deepest root that holds the document's path as the client names it, compared component by component without resolving links, regardless of primary priority, and returns no configuration outside all registered roots.
Diagnostics extend that lookup to a project's library files outside every
registered root, as a dependency outside the project folder is (the OSCAT
examples' OscatOop = { path = "../../../../libraries/oscat/oop" }): a source
file outside all registered roots that lies in a library folder of a
registered project is diagnosed under that project's configuration, with its
[diagnostics] settings, severity overrides, external diagnostics and the
library-surface rule of §7.6. The folders and the file are compared as §7.6
compares them, after resolving links. When library folders of several
projects hold the file, the deepest folder decides, then the higher
[workspace] priority, then the root URI that sorts first. Every other lookup
still returns no configuration for such a file. Each project's own and library
folders are resolved once, when its configuration is set; a document is
resolved only for a project with library folders and for a file outside every
registered root.
Document state follows these fail-closed rules:
- opening creates or promotes one tracked open document and updates the semantic project under the URI's source identity;
- indexing never overwrites an open editor buffer, and re-indexing identical closed content is a no-op;
- updating an unknown or non-open document is a no-op and must not create an untracked semantic-project source;
- closing an unknown document and removing an unknown document are no-ops;
- URI, file-ID, and document lookups remain bidirectionally consistent for tracked documents; and
- configuration-scoped file-ID projection includes path sources under that configuration's indexing roots and excludes sources outside them.
Disk deletion and indexing share one source-membership contract (tooling behavior, not a change to IEC 61131-3 Ed.3 §6.5.2.2 duplicate-declaration rules):
- watcher subscriptions include directory deletions, not only ST/configuration filenames; a deleted directory removes its indexed descendant sources, including sources whose closed document text was evicted from memory; eviction drops cached text but retains the registered URI and stable file identity until membership is removed. Deletion uses that identity even when the path or its symlink alias no longer exists or has changed target. Directory removal matches registered URI descendants as well as canonical source descendants; sibling sources are unaffected;
- a workspace reindex reconciles sources proven missing on disk even when the collected file list is empty or the indexing budget truncates the pass; absence from that collected list alone is not evidence of deletion;
- a reindex preserves open unsaved buffers and sources whose disk state cannot be determined, and never replaces an open buffer with cached disk contents;
- document and semantic-project membership transitions commit together under a consistent lock order; a close racing deletion cannot leave an orphan source;
- a disk indexing result cannot reintroduce a file already deleted before that result commits. Reusing a POU name in a new scaffold after deleting its old project therefore does not import the deleted declaration.
Every committed project-text mutation advances the document generation once and clears both call-hierarchy caches. No-op lifecycle notifications do neither. Call-hierarchy reads return owned snapshots, so caller mutation cannot alter a cached response.
Semantic request tickets are monotonically increasing generations. Starting or explicitly cancelling a request invalidates every older ticket. A cancelled ticket cannot insert or replace semantic-token or diagnostic cache state. Accepted semantic-token writes receive a new result ID and replace the URI's token snapshot. Diagnostic writes reuse a result ID only when both content and diagnostic hashes match; a change to either hash creates a new result ID.
Editor activity changes the never-recorded sentinel to a nonnegative elapsed age. The database callback holds a read view of the same project whose sources are managed by document lifecycle operations. These state-management rules are truST tooling behavior and do not alter IEC 61131-3 language semantics.
7.7 Command-line check (trust-lsp check)¶
trust-lsp check [--project DIR] [--format text|json] [--file PATH]... [--deny-warnings]
runs without an LSP client. It loads DIR as a workspace folder exactly as
initialize/initialized do (ProjectConfig::load, index_workspace) and reports, per
indexed file, the result of the same collect_diagnostics_with_ticket path that answers
textDocument/diagnostic, and for the project's configuration file (trust-lsp.toml) the
diagnostics the editor shows for it (L001–L007 and the configuration warnings), so CI
output and editor diagnostics agree; a broken library or dependency configuration fails the
check. --file narrows the report to the named source files. Text output is
path:line:column: severity[code]: message (one-based; columns in UTF-16 code units as in
LSP), sorted by path and position, with a summary on stderr. path is relative to DIR
and written as the platform writes paths (src\Main.st on Windows). JSON output is one
object: version, project, files (the source files checked), errors, warnings, diagnostics[] (path,
line, column, endLine, endColumn, severity, code, message). A --file that
is not one of the project's indexed source files, or a project without source files, is a
usage error: nothing is checked and the command names the file or directory. Exit status
0 without errors, 1 with errors (or warnings under --deny-warnings), 2 on usage or
I/O errors.
7.8 Command-line project command (trust-lsp command)¶
trust-lsp command <command> [--project DIR] runs one project command without an LSP
client, for a tool that needs the same project knowledge or makes the same project edit as
the editor does (Control Studio's loop setup and Keep in project, Specification 35 §0.1a,
B1 and B6.5). It reads the command's one
argument as a JSON object from standard input, loads DIR (default: the current folder)
exactly as trust-lsp check does (7.7), answers the argument through the same handler that
answers workspace/executeCommand, and writes that handler's JSON answer to standard
output as one line. The project is read from disk: the argument's open texts are refused,
because no editor is attached. The answer is the editor's own: edits are returned, never
applied, so the caller writes them. The commands it runs are those that only read the
project and answer facts or an edit: trust-lsp.hmiBindings (the compiled project's
control catalog and process image), trust-lsp.controlKeepParameters and
trust-lsp.hmiEquipment; another command is a usage error. Exit status 0 when the handler answered (an answer
with ok: false included), 2 on a usage error, an unreadable argument or an I/O error,
with the reason on standard error.
8. Runtime & Debugger¶
The workspace includes a runtime and debug adapter used for executing and testing ST programs. The authoritative specifications for these components are:
docs/specs/10-runtime-semantics.md
8.1 Runtime¶
- Runtime execution is defined by the ST runtime specification, including task scheduling, process image semantics, retain behavior, and fault handling.
- Production runtimes are started via the CLI using the project folder (runtime bundle format)
format (
trust-runtimeortrust-runtime run --project).trust-runtime buildwrites the project's program imageprogram.tpi(specification 44).
8.2 Debugger¶
- Debug adapter behavior follows DAP and the ST debugger specification.
- Breakpoints and stepping are statement-based and use source locations from the compiler.
9. Error Handling¶
9.1 Error Categories¶
enum DiagnosticSeverity {
Error, // Prevents compilation
Warning, // Potential issue
Info, // Informational
Hint, // Style suggestion
}
struct Diagnostic {
range: TextRange,
severity: DiagnosticSeverity,
code: DiagnosticCode,
message: String,
related: Vec<RelatedInfo>,
}
9.2 Error Codes¶
| Code | Category | Description |
|---|---|---|
| E001 | Syntax | Unexpected token |
| E002 | Syntax | Missing token |
| E003 | Syntax | Unclosed block |
| E101 | Name | Undefined variable |
| E102 | Name | Duplicate declaration |
| E103 | Name | Cannot resolve type |
| E201 | Type | Type mismatch |
| E202 | Type | Invalid operation |
| E203 | Type | Incompatible assignment |
| E208 | Type | Interface conformance |
| W001 | Warning | Unused variable |
| W002 | Warning | Unreachable code |
| W003 | Warning | Implicit conversion |
10. Performance Requirements¶
10.1 Latency Targets¶
| Operation | Target | Maximum |
|---|---|---|
| Keystroke response | < 16ms | 50ms |
| Completion list | < 50ms | 200ms |
| Go to definition | < 20ms | 100ms |
| Find references | < 100ms | 500ms |
| Full file diagnostics | < 200ms | 1000ms |
10.2 Memory Targets¶
| Metric | Target |
|---|---|
| Per-file overhead | < 10x source size |
| Idle memory | < 100MB |
| Large project (100 files) | < 500MB |
10.3 Optimization Strategies¶
- Incremental parsing - Salsa invalidation on changed files (file-level granularity)
- Cross-file dependency tracking - Salsa queries recompute only affected dependents
- Indexing budgets -
max_files/max_mslimits for large workspaces - Expression-level type cache - Cache
type_ofresults by expression hash + scope, invalidated when symbol tables change - Adaptive background indexing - Per-file throttling based on recent editor activity and observed indexing cost
- Memory budgets - Closed-document eviction with on-demand reload when over memory budget
- Progress reporting - Work-done progress notifications during indexing
- Request prioritization - Background workspace scans (
workspace/symbol,workspace/diagnostic, cross-file references) are concurrency-limited to keep interactive requests responsive
11. Testing Strategy¶
11.1 Test Categories¶
11.1.1 Unit Tests¶
- Lexer token output
- Parser tree structure
- Type checker rules
- Symbol resolution
11.1.2 Integration Tests¶
- Full file parsing
- Cross-file references
- LSP protocol compliance + golden handler responses
- Neovim and Zed editor smoke gates MUST verify that every exact Rust test
filter names a discovered
trust-lsptest before executing it. A missing or stale filter that selects zero tests MUST fail instead of being reported as editor workflow coverage. - Performance harness (ignored by default): hover/completion/rename budgets + large workspace indexing (Section 10 targets)
- VS Code extension integration tests for completion, formatting, and code actions (IEC 61131-3 Ed.3 §6.1-6.3; Tables 4-9; §6.5.2.2)
- Stdlib coverage check: ensures all IEC standard function/FB names appear in
docs/specs/coverage/standard-functions-coverage.md(IEC 61131-3 Ed.3, Tables 22–36, 43–46)
11.1.3 Snapshot Tests¶
- Parser output (insta)
- Diagnostic output
- Completion / signature / formatting results
11.2 Test Corpus¶
tests/corpus/
├── declarations/
│ ├── variables.st
│ ├── types.st
│ └── functions.st
├── expressions/
│ ├── arithmetic.st
│ ├── logical.st
│ └── comparison.st
├── statements/
│ ├── if.st
│ ├── case.st
│ ├── for.st
│ └── while.st
├── function_blocks/
│ ├── basic.st
│ ├── inheritance.st
│ └── interfaces.st
└── errors/
├── syntax_errors.st
└── type_errors.st
11.3 Fuzzing¶
- AFL/libFuzzer for parser robustness
- Grammar-aware fuzzing for valid-ish input
11.4 Benchmarks¶
- Large file parsing (10K+ lines)
- Completion response time
- Memory usage under load
- Benchmark evidence must record whether the runtime binary was built as a portable/generic release or with host-native CPU tuning.
- Official/shared release artifacts must remain portable; host-native builds (for example
-C target-cpu=native) are opt-in benchmark/tuning artifacts only and must not be treated as the default cross-host baseline.
12. Current Implementation Status¶
12.1 What's Implemented¶
- Lexer: Complete token set for IEC 61131-3 ST
- Parser: ST constructs parsed per specs (including ACTION blocks and AT addresses)
- Symbol Table: Scope-aware with namespaces and cross-file resolution
- Type Registry: Elementary, generic, and user-defined types (STRUCT/UNION/ENUM/ARRAY/STRING[n])
- Hover: Full implementation with type and qualifier display
- Go to Definition: Project-wide navigation (workspace indexed)
- Document Symbols: Flat list of declarations
- Debugger (DAP): Core DAP adapter with breakpoints, stepping, scopes, variables, evaluate, logpoints
12.2 Known Limitations¶
- Workspace
-
Workspace indexing runs on initialize; on-disk changes are tracked via file watching when supported by the client (otherwise require reload)
-
LSP
- Formatting does not wrap/reflow lines beyond operator spacing + VAR alignment
- Folding ranges are coarse (node-based regions)
- Debugger
- VS Code extension wiring exists, but the manual test plan is still pending
- Stepping is statement-level only; expressions are not single-stepped
- Debug evaluation is restricted to side-effect-free expressions and a small pure stdlib whitelist
- Hot reload is implemented via a custom request and supports per-resource reloads with retained globals preserved across warm restart
- I/O write/force/release supports both input and output areas through the
DAP/control bridge. Attach-mode runtimes use explicit Live Values custom
requests (
stIoWrite,stIoForce,stIoRelease) instead ofsetExpression
Appendix A: IEC 61131-3 Operator Precedence¶
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (lowest) | OR | Left |
| 2 | XOR | Left |
| 3 | AND, & | Left |
| 4 | =, <> | Left |
| 5 | <, >, <=, >= | Left |
| 6 | +, - | Left |
| 7 | *, /, MOD | Left |
| 8 | ** | Right |
| 9 (highest) | NOT, -(unary), +(unary) | Right |
Appendix B: Type Hierarchy¶
ANY
├── ANY_DERIVED
│ ├── ANY_ELEMENTARY
│ │ ├── ANY_MAGNITUDE
│ │ │ ├── ANY_NUM
│ │ │ │ ├── ANY_REAL
│ │ │ │ │ ├── REAL
│ │ │ │ │ └── LREAL
│ │ │ │ └── ANY_INT
│ │ │ │ ├── ANY_SIGNED
│ │ │ │ │ ├── SINT
│ │ │ │ │ ├── INT
│ │ │ │ │ ├── DINT
│ │ │ │ │ └── LINT
│ │ │ │ └── ANY_UNSIGNED
│ │ │ │ ├── USINT
│ │ │ │ ├── UINT
│ │ │ │ ├── UDINT
│ │ │ │ └── ULINT
│ │ │ └── ANY_DURATION
│ │ │ ├── TIME
│ │ │ └── LTIME
│ │ ├── ANY_BIT
│ │ │ ├── BOOL
│ │ │ ├── BYTE
│ │ │ ├── WORD
│ │ │ ├── DWORD
│ │ │ └── LWORD
│ │ ├── ANY_STRING
│ │ │ ├── STRING
│ │ │ └── WSTRING
│ │ ├── ANY_DATE
│ │ │ ├── DATE
│ │ │ └── LDATE
│ │ └── ANY_DATE_AND_TIME
│ │ ├── DT
│ │ └── LDT
│ └── USER_DEFINED
│ ├── STRUCT
│ ├── ENUM
│ ├── ARRAY
│ └── FUNCTION_BLOCK
└── ANY_POINTER
├── POINTER TO ...
└── REF_TO ...
Appendix C: PLCopen XML Interchange (ST-Complete)¶
Runtime exposes an ST-complete PLCopen XML profile through trust-runtime plcopen:
trust-runtime plcopen profileprints the supported profile contract.trust-runtime plcopen exportexports ST project content to PLCopen XML.trust-runtime plcopen importimports supported PLCopen ST project content intosrc/:- ST POUs (
PROGRAM,FUNCTION,FUNCTION_BLOCK) - supported
types/dataTypessubset (elementary,derived,array,struct,enum,subrange) materialized as generatedTYPEdeclarations - project model declarations in
instances/configurations/resources/tasks/program instances - CODESYS/e!COCKPIT vendor dialect constructs (import):
- derived names with
REFERENCE TO/POINTER TOprefixes project to native pointer/reference declarations (the analyzer owns auto-deref andREF=semantics); array dimension bounds pass through verbatim as constant expressions datatypeinheritanceaddData (<Extends>Base</Extends>) emitsTYPE x EXTENDS y; an unprojectable marker skips the dataType fail-closed (PLCO404)pouinheritanceaddData emitsEXTENDS/IMPLEMENTSon FUNCTION_BLOCK declarations (synthesized headers and raw ST bodies alike)- project-level
<Interface>objects import as declaration-onlyINTERFACEblocks (METHOD signature stubs,EXTENDShonored, properties rendered as METHODs) globalVarscontainer qualifiers (retain/persistent/constant) carry into the renderedVAR_GLOBALheader- e!COCKPIT
MixedAttrsVarListattributes views and identical whole-list duplicate copies are merged into the primary list (PLCO223) instead of multiplying into_N.stfiles
- derived names with
trust-runtime plcopen importemits a migration report atinterop/plcopen-migration-report.jsonwith:- discovered/imported/skipped POU counts
- imported type/project-model counts (
imported_data_types,imported_interfaces,discovered_configurations,imported_configurations,imported_resources,imported_tasks,imported_program_instances) - source coverage (% imported/discovered)
- semantic-loss score (weighted from skipped POUs + unsupported nodes/warnings)
- compatibility coverage summary (
supported_items,partial_items,unsupported_items,support_percent,verdict) - structured unsupported diagnostics (
code,severity,node,message, optionalpou,action) - applied vendor-library shim summary (
vendor,source_symbol,replacement_symbol,occurrences,notes) - per-POU entry status (
importedorskipped) and skip reasons
Current ST-complete contract:
- Namespace:
http://www.plcopen.org/xml/tc6_0200 - Profile:
trust-st-complete-v1 - Supported POU body:
STtext bodies forPROGRAM,FUNCTION,FUNCTION_BLOCK - Supported
dataTypesbaseType subset:elementary,derived,array,struct,enum,subrange - Supported project model:
instances/configurations/resources/tasks/program instances - Source mapping: embedded
addDatapayload + sidecar*.source-map.json - Unsupported nodes: reported as diagnostics and preserved via vendor extension hooks where applicable
- Vendor-variant import aliases:
PROGRAM/PRG->programFUNCTION/FC/FUN->functionFUNCTION_BLOCK/FB->functionBlock- Vendor ecosystem detection heuristics for migration reports:
codesys,beckhoff-twincat,siemens-tia,rockwell-studio5000,schneider-ecostruxure,mitsubishi-gxworks3, fallbackgeneric-plcopen- Vendor-library baseline shim catalog includes selected alias normalization
(e.g., Siemens
SFB3/4/5->TP/TON/TOF) with per-import diagnostics.
Deliverable 5 parity fixture gate:
- CODESYS ST fixture pack (
small/medium/large) with deterministic expected migration artifacts under: crates/trust-runtime/tests/fixtures/plcopen/codesys_st_complete/- Schema-drift parity regression test:
crates/trust-runtime/tests/plcopen_st_complete_parity.rs
Round-trip limits and known gaps are documented in
docs/guides/PLCOPEN_INTEROP_COMPATIBILITY.md.
Appendix D: References¶
- IEC 61131-3:2013 - Programmable controllers - Part 3: Programming languages
- PLCopen - Technical Committee 6 (XML)
- CODESYS Online Help - https://help.codesys.com
- Beckhoff InfoSys - https://infosys.beckhoff.com
- rust-analyzer Architecture - https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/architecture.md