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, bytecode format, and 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 and bytecode decoding - 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 + bytecode
└── 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 |
| 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; TIME/DATE types per IEC 61131-3 Ed.3 §6.4.2 (Table 10) | docs/specs/09-semantic-rules.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 time/date and direct-I/O usageW012shared 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 HMI command and descriptor diagnostics¶
- HMI commands reject malformed argument shapes and invalid styles before filesystem mutation. Valid initialization and binding requests return the deterministic scaffold and external binding catalogue.
- HMI TOML diagnostics use open-buffer content, accept valid pages, and report invalid widget, type, property, and binding data without requiring a successful runtime compile.
- Near-match suggestions rank the closest valid binding first and suppress low-confidence noise.
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 |
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,library_paths,vendor_profile(dialect + formatting presets), andstdlibselection.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).
[[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.[[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.[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; andC005: a diagnostic severity override has a blank code or unknown severity and was ignored.
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 matching root, regardless of primary priority, and returns no configuration outside all registered roots.
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.
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.
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). Project folders can be generated bytrust-runtime build(preferred) or CI tooling that emits STBC.
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 |
| 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 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,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