Skip to content

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

  1. Overview
  2. Architecture
  3. Lexer Specification
  4. Parser Specification
  5. Semantic Analysis
  6. IDE Features
  7. LSP Protocol
  8. Runtime & Debugger
  9. Error Handling
  10. Performance Requirements
  11. Testing Strategy
  12. 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

  1. Correctness - Accurately parse and analyze valid ST code
  2. Error Tolerance - Provide useful feedback even for invalid code
  3. Performance - Sub-100ms response times for interactive features
  4. Incrementality - Re-analyze only what changed
  5. 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 VAR etc. - 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:

  • W003 unreachable code after unconditional terminators or constant-false branches
  • W004 missing ELSE in CASE
  • W005 implicit conversion
  • W008 cyclomatic complexity quality lint
  • W009 unused POU quality lint
  • W010/W011 non-deterministic time/date and direct-I/O usage
  • W012 shared global access across scheduled tasks
  • W013/W014 numeric 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/diagnostic request returns ContentModified;
  • a cancelled workspace/diagnostic request returns ContentModified for 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_EXTERNAL resolves to the matching VAR_GLOBAL declared 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.shared resolve 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 maps Namespace.Path<workspace>/Namespace/Path.st unless 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 (invokes trust-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/USING lines; invokes trust-lsp.moveNamespace via 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 stlsp section takes precedence over legacy trust-lsp aliases. 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 TextDocumentContentChangeEvent ranges.
  • 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 didChange is a notification, the server logs the reason and sends window/showMessage with 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_profile presets 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 maxLineLength and 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/gxworks3 use 4-space indents with spaced operators; siemens uses 2-space indents with compact operator spacing; all align END_* keywords by default.

7.6 Project Configuration & Workspace Indexing

  • Per-root project config file: trust-lsp.toml, .trust-lsp.toml, or trustlsp.toml.
  • [project] supports include_paths, library_paths, vendor_profile (dialect + formatting presets), and stdlib selection.
  • stdlib profiles: 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_profile is set and no explicit stdlib allow-list/profile is provided, the server defaults to the IEC profile for completions/hover.
  • [[libraries]] entries include name, path, and optional version for external library indexing.
  • [dependencies] supports local and git package references:
  • local: Name = "path" or Name = { 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, and trust-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/branch pin git dependencies explicitly.
  • build.dependencies_locked = true requires explicit pinning or a matching lock entry.
  • Resolver snapshots pinned sources to build.dependency_lockfile (default trust-lsp.lock) for reproducible resolution.
  • build.dependencies_offline = true disables 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 declare dependencies (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 declare docs (array of markdown files) to attach vendor library documentation to hover/completion. Each file uses # SymbolName headings followed by doc text.
  • [workspace] controls multi-root federation: priority orders root results for workspace symbol search, and visibility (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 optional target/profile defaults.
  • [[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_dir overrides the cache location. Cache reuse checks file metadata and stored content hashes.
  • [indexing] memory budget controls: memory_budget_mb caps closed-document index memory (MB) and evict_to_percent defines the LRU eviction target; evicted documents are reloaded on demand when accessed.
  • [indexing] adaptive throttling: throttle_idle_ms, throttle_active_ms, throttle_max_ms, and throttle_active_window_ms pace background indexing based on recent editor activity and observed per-file work.
  • [runtime] supports control_endpoint and optional control_auth_token for 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_pack presets safety-focused defaults (e.g., iec-safety, siemens-safety, codesys-safety, beckhoff-safety, twincat-safety, mitsubishi-safety, gxworks3-safety); explicit warn_* keys override pack defaults. [diagnostics].severity_overrides can 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_paths lists JSON diagnostics payloads from external linters (optional per-diagnostic fix data yields quick-fix actions).
  • Vendor diagnostic defaults: siemens disables Missing ELSE (W004) and implicit conversion (W005); codesys, beckhoff, twincat, mitsubishi, and gxworks3 keep 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/workDoneProgress when 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 to full;
  • 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 to public; and
  • C005: 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-runtime or trust-runtime run --project). Project folders can be generated by trust-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

  1. Incremental parsing - Salsa invalidation on changed files (file-level granularity)
  2. Cross-file dependency tracking - Salsa queries recompute only affected dependents
  3. Indexing budgets - max_files / max_ms limits for large workspaces
  4. Expression-level type cache - Cache type_of results by expression hash + scope, invalidated when symbol tables change
  5. Adaptive background indexing - Per-file throttling based on recent editor activity and observed indexing cost
  6. Memory budgets - Closed-document eviction with on-demand reload when over memory budget
  7. Progress reporting - Work-done progress notifications during indexing
  8. 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-lsp test 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

  1. Workspace
  2. Workspace indexing runs on initialize; on-disk changes are tracked via file watching when supported by the client (otherwise require reload)

  3. LSP

  4. Formatting does not wrap/reflow lines beyond operator spacing + VAR alignment
  5. Folding ranges are coarse (node-based regions)
  6. Debugger
  7. VS Code extension wiring exists, but the manual test plan is still pending
  8. Stepping is statement-level only; expressions are not single-stepped
  9. Debug evaluation is restricted to side-effect-free expressions and a small pure stdlib whitelist
  10. Hot reload is implemented via a custom request and supports per-resource reloads with retained globals preserved across warm restart
  11. 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 of setExpression

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 profile prints the supported profile contract.
  • trust-runtime plcopen export exports ST project content to PLCopen XML.
  • trust-runtime plcopen import imports supported PLCopen ST project content into src/:
  • ST POUs (PROGRAM, FUNCTION, FUNCTION_BLOCK)
  • supported types/dataTypes subset (elementary, derived, array, struct, enum, subrange) materialized as generated TYPE declarations
  • project model declarations in instances/configurations/resources/tasks/program instances
  • trust-runtime plcopen import emits a migration report at interop/plcopen-migration-report.json with:
  • 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, optional pou, action)
  • applied vendor-library shim summary (vendor, source_symbol, replacement_symbol, occurrences, notes)
  • per-POU entry status (imported or skipped) and skip reasons

Current ST-complete contract:

  • Namespace: http://www.plcopen.org/xml/tc6_0200
  • Profile: trust-st-complete-v1
  • Supported POU body: ST text bodies for PROGRAM, FUNCTION, FUNCTION_BLOCK
  • Supported dataTypes baseType subset: elementary, derived, array, struct, enum, subrange
  • Supported project model: instances/configurations/resources/tasks/program instances
  • Source mapping: embedded addData payload + sidecar *.source-map.json
  • Unsupported nodes: reported as diagnostics and preserved via vendor extension hooks where applicable
  • Vendor-variant import aliases:
  • PROGRAM/PRG -> program
  • FUNCTION/FC/FUN -> function
  • FUNCTION_BLOCK/FB -> functionBlock
  • Vendor ecosystem detection heuristics for migration reports:
  • codesys, beckhoff-twincat, siemens-tia, rockwell-studio5000, schneider-ecostruxure, mitsubishi-gxworks3, fallback generic-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

  1. IEC 61131-3:2013 - Programmable controllers - Part 3: Programming languages
  2. PLCopen - Technical Committee 6 (XML)
  3. CODESYS Online Help - https://help.codesys.com
  4. Beckhoff InfoSys - https://infosys.beckhoff.com
  5. rust-analyzer Architecture - https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/architecture.md