{"slug": "an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects", "title": "An LSP for Tcl 8.4-9.1, F5 iRules, F5 iApps and other Tcl dialects", "summary": "A new language server for Tcl 8.4-9.1 and F5 iRules/iApps dialects, written in Python using pygls, provides multi-editor support via stdio and includes 25+ commands, 16 built-in snippets, and an MCP context server exposing 44 analysis tools. The project, hosted on GitHub by bitwisecook, offers installation guides for VS Code, Neovim, Zed, Emacs, Helix, Sublime Text, and JetBrains, with a one-line curl installer for the tcl and f5 CLIs.", "body_md": "A language server for Tcl with multi-editor support.\n\nThe server is written in Python using [pygls](https://github.com/openlawlibrary/pygls)\nand communicates over stdio, making it compatible with any LSP client.\n\nInstallation guides:[INSTALL-editors.md]— step-by-step setup for VS Code, Neovim, Zed, Emacs, Helix, Sublime Text, and JetBrains on macOS (Homebrew), Linux (Debian/Ubuntu, RHEL/CentOS, Fedora), and Windows.[INSTALL-cli.md]— the`tcl`\n\nand`f5`\n\nCLIs, including a one-line`curl | sh`\n\ninstaller.\n\n| Editor | Type | Setup | Unique extras |\n|---|---|---|---|\n|\n\n`.vsix`\n\nfrom Releases`@irule`\n\n/`@tcl`\n\n/`@tk`\n\nCopilot chat, 25+ commands[Neovim](/bitwisecook/tcl-lsp/blob/main/editors/neovim)`tcl_lsp.lua`\n\nto `~/.config/nvim/server/`\n\n[Zed](/bitwisecook/tcl-lsp/blob/main/editors/zed)`/tcl-doc`\n\nand `/irule-event`\n\nslash commands[Emacs](/bitwisecook/tcl-lsp/blob/main/editors/emacs)`init.el`\n\nfor eglot or lsp-mode[Helix](/bitwisecook/tcl-lsp/blob/main/editors/helix)`~/.config/helix/languages.toml`\n\n[Sublime Text](/bitwisecook/tcl-lsp/blob/main/editors/sublime-text)[JetBrains](/bitwisecook/tcl-lsp/blob/main/editors/jetbrains)All editors connect to the same Python LSP server over stdio. The server can\nbe invoked from source (`uv run python -m server`\n\n) or as a standalone zipapp\n(`python3 tcl-lsp-server.pyz`\n\n).\n\n**Also documented in INSTALL-editors.md:**\n\n*VS Code-compatible editors*(load the same`.vsix`\n\nunchanged) — Cursor, Windsurf, VSCodium, code-server / Coder, GitHub Codespaces, Gitpod, and Eclipse Theia.*Other LSP-capable editors*(point a generic LSP client at the`.pyz`\n\n) — Vim (vim-lsp or coc.nvim), Kate, Kakoune, Notepad++, Geany, Lite XL, micro, CudaText, JupyterLab, Doom Emacs, and Spacemacs.\n\n**File types recognised:** `.tcl`\n\n, `.tk`\n\n, `.itcl`\n\n, `.tm`\n\n, `.irul`\n\n, `.irule`\n\n,\n`.iapp`\n\n, `.iappimpl`\n\n, `.impl`\n\n, `.apl`\n\n, `.exp`\n\n, plus shebang detection for\n`#!/usr/bin/tclsh`\n\n, `#!/usr/bin/wish`\n\n, and `#!/usr/bin/expect`\n\n.\nFiles named `presentation`\n\n(no extension) are auto-detected as APL.\nPer-file `# tcl-dialect:`\n\ncomment directives pin a specific dialect.\n\nThe full-featured extension, distributed as a `.vsix`\n\n, bundles the LSP server\nand provides the richest integration.\n\n**25+ commands** including: Restart Server, Select Dialect, Apply Safe Quick\nFixes, Apply All Optimisations, Open in Tcl Compiler Explorer, Open Tk Preview,\nFormat Document, Minify Document, Insert iRule Event Skeleton, Scaffold Tcl\nPackage Starter, Insert `package require`\n\n, Run Runtime Validation, Translate\niRule to F5 XC, Extract iRule from Config, Escape/Unescape Selection, Base64\nEncode/Decode Selection.\n\n**Keyboard shortcuts:** Ctrl+Alt+O (optimise), Ctrl+Alt+M (minify),\nCtrl+Alt+E (compiler explorer).\n\n**Status bar:** shows the active dialect (clickable to change) and the\nextension version.\n\nInstall: see [INSTALL-editors.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md#vs-code).\n\nZero-plugin setup on Neovim 0.11+ using the native LSP client. Also works\nwith nvim-lspconfig (0.8+) or a manual `FileType`\n\nautocommand.\n\n```\n-- ~/.config/nvim/server/tcl_lsp.lua  (Neovim 0.11+)\nreturn {\n  cmd = { \"python3\", \"/path/to/tcl-lsp-server.pyz\" },\n  filetypes = { \"tcl\" },\n  settings = {\n    tclLsp = {\n      dialect = \"tcl8.6\",\n      formatting = { indentSize = 4, maxLineLength = 120 },\n    },\n  },\n}\n\n-- init.lua\nvim.filetype.add({ extension = { tcl = \"tcl\", irul = \"tcl\", irule = \"tcl\" } })\nvim.lsp.enable(\"tcl_lsp\")\n```\n\nA full Zed extension that auto-downloads the server zipapp from GitHub Releases on first use and auto-discovers Python 3.10+ on your PATH.\n\nIncludes 16 built-in snippets (`tcl-proc`\n\n, `tcl-namespace`\n\n, `tcl-if`\n\n,\n`irule-http-request`\n\n, `irule-collect-release`\n\n, etc.), an MCP context server\nexposing all 44 analysis tools, and slash commands (`/tcl-doc`\n\n, `/irule-event`\n\n,\n`/tcl-validate`\n\n).\n\nInstall: see [INSTALL-editors.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md#zed).\n\nWorks with the built-in **eglot** client (Emacs 29+) or **lsp-mode**.\n\n```\n;; eglot (Emacs 29+)\n(with-eval-after-load 'eglot\n  (add-to-list 'eglot-server-programs\n               '(tcl-mode . (\"python3\" \"/path/to/tcl-lsp-server.pyz\"))))\n(add-hook 'tcl-mode-hook #'eglot-ensure)\n\n;; Settings\n(setq-default eglot-workspace-configuration\n              '(:tclLsp (:dialect \"tcl8.6\"\n                         :formatting (:indentSize 4 :maxLineLength 120))))\n```\n\nMinimal TOML configuration in `~/.config/helix/languages.toml`\n\n.\n\n```\n[language-server.tcl-lsp]\ncommand = \"python3\"\nargs = [\"/path/to/tcl-lsp-server.pyz\"]\n\n[language-server.tcl-lsp.config.tclLsp]\ndialect = \"tcl8.6\"\n\n[[language]]\nname = \"tcl\"\nscope = \"source.tcl\"\nfile-types = [\"tcl\", \"tk\", \"itcl\", \"tm\", \"irul\", \"irule\", \"iapp\"]\nlanguage-servers = [\"tcl-lsp\"]\n```\n\nA full Sublime Text package (`.sublime-package`\n\n) that works in two modes:\nstandalone (syntax highlighting + 16 snippets + static completions) and\nenhanced (full LSP features when the LSP package is installed).\n\nAuto-discovers the bundled `.pyz`\n\nserver from the package archive.\n\nInstall: see [INSTALL-editors.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md#sublime-text).\n\n**Commands:** Select Dialect, Restart Language Server, Format Document, Minify\nDocument, Apply Safe Quick Fixes, Apply All Optimisations.\n\nA full IntelliJ Platform plugin (`.zip`\n\n) for IntelliJ IDEA 2024.1+ and other\nJetBrains IDEs. Includes a dedicated settings panel (Settings > Tools > Tcl\nLanguage Server) with toggles for every feature, diagnostic code, and\nformatting option.\n\nFeatures a **Compiler Explorer tool window** with JCEF browser for inspecting\nIR, CFG, SSA, and optimiser output directly inside the IDE.\n\nInstall: see [INSTALL-editors.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md#jetbrains).\nBuild from source: `make build-editor-jetbrains`\n\n.\n\nFast syntax feedback fires immediately on every keystroke; deeper semantic, optimiser, and security analysis runs in the background and merges results as each tier completes.\n\n```\n# Tier 1 (instant): syntax errors — missing brace caught on parse\nproc broken {x {\n    puts $x\n}\n\n# Tier 2 (background): semantic — arity mismatch flagged after analysis\nstring length \"a\" \"b\"   ;# E003: too many arguments\n```\n\nVariables, procs, keywords, and strings are classified using SSA-informed type\ninformation, giving richer highlighting than a TextMate grammar alone. The\nserver provides 44 token types beyond the standard LSP set, including\nsub-token highlighting inside strings. Tokens are cached per top-level chunk\nso only dirty regions are recomputed after an edit, and the server supports\n`textDocument/semanticTokens/full/delta`\n\nfor bandwidth-efficient incremental\nupdates.\n\n```\nnamespace eval app {\n    variable count 0            ;# 'count' highlighted as variable\n    proc handle {request} {     ;# 'handle' highlighted as function\n        incr count              ;# 'incr' highlighted as keyword\n        puts \"req: $request\"    ;# '$request' highlighted as variable inside string\n    }\n}\n```\n\nIn addition to standard token types (keyword, function, variable, string, comment, number, operator, parameter, namespace), the server provides domain-specific token types:\n\n| Category | Token types | Example |\n|---|---|---|\nRegexp |\n`regexpGroup` , `regexpCharClass` , `regexpQuantifier` , `regexpAnchor` , `regexpEscape` , `regexpBackref` , `regexpAlternation` |\n`regexp {(\\d+)\\s+(\\w+)} $line` — each part gets distinct highlighting |\nFormat strings |\n`formatPercent` , `formatSpec` , `formatFlag` , `formatWidth` |\n`format \"%- 10.2f\" $val` — `%` , `-` , `10.2` , and `f` each highlighted |\nBinary format |\n`binarySpec` , `binaryCount` , `binaryFlag` |\n`binary scan $data su3 x y z` — `s` , `u` , and `3` each highlighted |\nClock format |\n`clockPercent` , `clockSpec` , `clockModifier` |\n`clock format $t -format \"%Y-%m-%d\"` — `%` , `Y` , `m` , `d` each highlighted |\nEscape sequences |\n`escape` |\n`puts \"line1\\nline2\\t${var}\"` — `\\n` , `\\t` highlighted inside strings |\nBIG-IP config |\n`object` , `ipAddress` , `port` , `partition` , `pool` , `monitor` , `profile` , `vlan` , `fqdn` , `routeDomain` , `encrypted` , `interface` |\nBIG-IP `.conf` files get object-aware highlighting |\n\nArity errors, unknown subcommands, best-practice violations, and security\nissues are reported with precise ranges. Diagnostics can be suppressed\ninline, per-file, per-project, per-editor, or globally — see\n[Suppressing diagnostics](#suppressing-diagnostics).\n\n```\nstring frobulate $x          ;# W001: unknown subcommand 'frobulate'\nset y [expr $a + $b]         ;# W100: unbraced expr (double-substitution risk)\neval $user_input             ;# W101: eval with substituted arguments (injection risk)\ncatch { error \"oops\" }       ;# W302: catch without a result variable\n```\n\nContext-aware completions for commands, subcommands, variables, proc names\n(workspace-wide), switch arms, and `package require`\n\nnames.\n\n```\nstring len|              ;# offers: length, last, ...\nset name \"world\"\nputs $na|                ;# offers: $name\ndict |                   ;# offers: create, get, set, exists, ...\n```\n\nHovering on a command, proc call, variable, or operator shows its signature,\ndoc comment, and type information. Multi-line docstrings are supported, and\n`@param`\n\n, `@return`\n\n, and `@brief`\n\ntags are parsed and displayed as structured\nmarkdown. Docstrings can appear above the proc or inside the proc body.\n\n```\n# @brief Greet a person by name.\n# @param name - Who to greet\n# @return The greeting string\nproc greet {name} {\n    return \"Hello, $name!\"\n}\n\ngreet \"Alice\"     ;# hover on 'greet' shows signature + formatted @param/@return docs\n```\n\nJump to the definition of a proc or variable — works across files in the workspace.\n\n```\nproc helper {} { return 42 }\nset x [helper]       ;# Ctrl+Click on 'helper' → jumps to proc definition above\nputs $x              ;# Ctrl+Click on '$x' → jumps to the set statement\n```\n\nLocate every usage of a proc or variable, including inside nested braced\nscript bodies such as `if`\n\n, `foreach`\n\n, and `namespace eval`\n\n.\n\n```\nproc add {a b} { expr {$a + $b} }\nset sum [add 1 2]       ;# ← reference to 'add'\nputs [add 3 4]           ;# ← reference to 'add'\n# \"Find all references\" on 'add' highlights all three locations\n```\n\nInspect incoming callers and outgoing callees for any procedure.\n\n```\nproc validate {input} { return [string is integer $input] }\nproc process {data}   { if {[validate $data]} { store $data } }\nproc main {}          { process \"42\" }\n\n# Incoming calls to 'validate': process\n# Outgoing calls from 'process': validate, store\n```\n\nSafely rename a proc or variable across all scopes in the file.\n\n```\nproc greeting {name} {\n    puts \"Hi, $name\"\n}\ngreeting \"World\"\n# Rename 'greeting' → 'salute' updates the proc definition and all call sites\n```\n\nAs you type arguments, the server shows the expected parameter list with the active parameter highlighted.\n\n```\nproc connect {host port {timeout 30}} { ... }\nconnect \"db.local\" |\n#                  ↑ signature help shows: connect (host port ?timeout?)\n#                    with 'port' highlighted as the active parameter\n```\n\nInline annotations show inferred types, format-string specifier meanings, and parameter names.\n\n```\nset count 42                          ;# inlay: ': int'\nset msg [format \"%s has %d items\" $name $count]\n#                 ↑ '%s → string'  ↑ '%d → integer'\n```\n\nA structured outline of the current file — procs, namespaces, variables — for quick navigation (Ctrl+Shift+O / Cmd+Shift+O).\n\n```\nnamespace eval app {\n    variable config {}         ;# symbol: app::config (variable)\n    proc init {} { ... }       ;# symbol: app::init (function)\n    proc run {} { init }       ;# symbol: app::run (function)\n}\n# Outline: app (namespace) → config, init, run\n```\n\nSearch for procs and variables across all open files in the workspace (Ctrl+T / Cmd+T).\n\n```\n# File: utils.tcl\nproc ::utils::parse_csv {data} { ... }\n\n# File: main.tcl\n# Type \"parse_csv\" in workspace symbol search → jumps to utils.tcl\n```\n\nCollapse proc bodies, control-flow blocks, multi-line comments, and namespace bodies.\n\n```\n# ── Header comment ──          ← foldable\n# Author: ...\nproc calculate {x} {            ← foldable\n    if {$x > 0} {               ← foldable\n        return [expr {$x * 2}]\n    }\n}\n```\n\nSmart expand/shrink selection by syntactic structure (Alt+Shift+→ / Alt+Shift+←).\n\n```\nproc greet {name} {\n    puts \"Hello $name\"\n}\n# Cursor on 'name' inside puts → expand: \"$name\" → \"Hello $name\" → puts command → proc body → proc → file\n```\n\n`source`\n\npaths and `package require`\n\nnames become clickable links that\nnavigate to the resolved file or package.\n\n```\npackage require http        ;# click → opens http package source\nsource lib/utils.tcl        ;# click → opens lib/utils.tcl\n```\n\nFull-document and range formatting with 25 configurable options. Defaults\nfollow the F5 iRules Style Guide. Supports full-document\n(`textDocument/formatting`\n\n) and range (`textDocument/rangeFormatting`\n\n)\nrequests.\n\n```\n# Before:\nproc messy { x  }  {\nif {$x>0}{return $x }\n   set y   [expr $x+1]  ;  puts $y }\n\n# After (formatted):\nproc messy {x} {\n    if {$x > 0} {\n        return $x\n    }\n    set y [expr $x + 1]\n    puts $y\n}\n```\n\nCapabilities include indentation (spaces or tabs, configurable size),\nbrace placement (K&R), expression bracing enforcement, variable\nbracing (`$var`\n\n→ `${var}`\n\n), line-length wrapping, semicolon splitting,\nsingle-line body expansion, blank-line normalisation between procs and\nblocks, comment alignment, trailing whitespace trimming, and line-ending\nnormalisation (LF/CRLF/CR).\n\n```\n# Expression bracing enforcement (enforceBracedExpr = true):\nif {$x > 0} { ... }       ;# ✓ braced\nif $x>0 { ... }           ;# → rewritten to: if {$x > 0} { ... }\n\n# Variable bracing (enforceBracedVariables = true):\nputs $name                 ;# → rewritten to: puts ${name}\n```\n\nQuick-fix actions are offered for diagnostics that have automated repairs. Refactoring actions are available on selected code.\n\n```\nexpr $a + $b         ;# W100 → quick-fix: wrap in braces → expr {$a + $b}\ncatch { error \"x\" }  ;# W302 → quick-fix: add result variable → catch { error \"x\" } result\nset x [expr {$x+1}]  ;# O114 → quick-fix: use incr idiom → incr x\n```\n\n**Extract to proc** — select one or more lines, trigger code actions\n(`Ctrl+.`\n\n), and choose *Extract selection into proc*. The selected code\nmoves into a new `proc`\n\nwith detected variable parameters; the original\nlines are replaced with a call. The editor places the cursor on the new\nproc name so you can rename it immediately.\n\nBundled code templates for Tcl structures and iRules event skeletons with secure defaults, collect/release pairs, and common patterns.\n\n```\n# Type 'proc' + Tab:\nproc name {args} {\n    # body\n}\n\n# Type 'when' + Tab (iRules):\nwhen HTTP_REQUEST {\n    # handler\n}\n```\n\nSwitch between Tcl 8.4/8.5/8.6/9.0, F5 iRules, F5 iApps, and EDA tooling\nprofiles. Tk, tcllib, and stdlib commands activate automatically when their\n`package require`\n\nappears. F5 iRules metadata follows BIG-IP command/event\nsource data, including profile aliases used by newer namespaces and events,\nshared TLS helper profiles such as `PERSIST`\n\n, and protocol namespace layer\nmetadata that stays aligned with the enabling profile stack.\n\n```\n# With dialect = tcl8.6:\ntry {\n    open $path r         ;# ✓ known in 8.6\n} on error {msg} {\n    puts $msg\n}\n\n# With dialect = tcl8.5:\ntry { ... }              ;# W002: command disabled in active dialect (try requires 8.6)\n```\n\nFull TclOO class hierarchy analysis with method resolution order (MRO), class definition tracking, and object-aware introspection.\n\n```\noo::class create Animal {\n    variable name\n    constructor {n} { set name $n }\n    method speak {} { return \"$name says ...\" }\n}\noo::class create Dog {\n    superclass Animal\n    method speak {} { return \"[my name] says woof!\" }\n}\n# Hover on 'Dog' shows class hierarchy: Dog -> Animal -> oo::object\n# Go-to-definition on 'speak' jumps to the method body\n# Type hierarchy shows Dog as a subtype of Animal\n```\n\nFeatures include class definition and method hover, go-to-definition for methods and constructors, type hierarchy (supertypes and subtypes), MRO computation matching C Tcl's algorithm, mixin and filter chain support, private variable and method visibility (TIP 500), and property/configurable support (TIP 558). The VM executes TclOO code with 85% native test conformance against the Tcl 9.0.3 oo.test suite.\n\nThe server lowers source to an intermediate representation, builds a control-flow graph, converts to SSA form, and runs type inference — all used to power deeper diagnostics and the optimiser.\n\n```\nproc fibonacci {n} {\n    set a 0; set b 1\n    for {set i 0} {$i < $n} {incr i} {\n        set t $b\n        set b [expr {$a + $b}]\n        set a $t\n    }\n    return $a\n}\n# IR → CFG → SSA → SCCP → liveness → type inference → bytecode\n```\n\nThe WASM code generator uses a per-proc **var-escape analysis** to decide\nwhich Tcl variables can stay in fast WASM locals and which must spill to\nthe runtime frame so `uplevel`\n\n, `upvar`\n\n, `eval`\n\n, and dynamic `set $name`\n\ncan see them by name. Procs that provably never let a variable escape pay\nzero frame-sync overhead on interpreter fallbacks. See the\n[design doc](/bitwisecook/tcl-lsp/blob/main/docs/design/compiler/var-escape-analysis.md) and the\n[KCS note](/bitwisecook/tcl-lsp/blob/main/docs/kcs/features/kcs-feature-var-escape-analysis.md) for the\nrules and the interprocedural propagation of callee `upvar`\n\nsources.\n\nTwenty-plus optimisation passes detect constant propagation, dead code, redundant computations, loop-invariant hoisting, strength reduction, and idiomatic rewrites — each offered as a quick-fix code action.\n\n```\n# O102 — constant expression folding:\nset a 1\nset b [expr {$a + 2}]   ;# → suggestion: replace with 'set b 3'\n\n# O114 — incr idiom recognition:\nset x [expr {$x + 1}]   ;# → suggestion: replace with 'incr x'\n\n# O105 — constant var-ref propagation / redundant computation (GVN/CSE):\nset a [expr {$x + $y}]\nset b [expr {$x + $y}]  ;# → suggestion: replace with 'set b $a'\n\n# O106 — loop-invariant code motion (LICM):\nfor {set i 0} {$i < $n} {incr i} {\n    set len [string length $fixed]   ;# → suggestion: hoist above the loop\n    lappend result $len\n}\n```\n\nTracks each variable's Tcl internal representation through the SSA type lattice. When a command forces a type conversion (\"shimmer\"), the performance cost is reported — especially inside loops.\n\n```\n# S100 — single shimmer (info):\nset x \"hello\"\nset n [llength $x]       ;# 'x' shimmers from STRING → LIST\n\n# S101 — shimmer inside loop (warning):\nset s \"42\"\nfor {set i 0} {$i < 1000} {incr i} {\n    set v [expr {$s + $i}]   ;# 's' shimmers STRING → INT on every iteration\n}\n\n# S102 — type thunking (warning):\nfor {set i 0} {$i < 100} {incr i} {\n    set n [llength $data]     ;# 'data' shimmers STRING → LIST\n    set data \"updated $n\"     ;# 'data' back to STRING — oscillates each iteration\n}\n```\n\nS100–S102 are *performance* warnings. **S110** is a *correctness* warning for\nbyte-array corruption: binary data (a `binary format`\n\nresult or an iRules\n`*::payload`\n\nbyte array) that is forced through character-string semantics and\nthen written back as bytes silently re-encodes every byte `≥ 0x80`\n\n. This is the\ncanonical iRules payload-rewrite bug ([F5 KB K22406348](https://my.f5.com/manage/s/article/K22406348)).\n\n```\n# S110 — byte-array corruption (warning):\nwhen HTTP_REQUEST_DATA {\n    set body [HTTP::payload]\n    set body \"$body INJECTED\"          ;# byte array decoded to a character string\n    HTTP::payload replace 0 100 $body  ;# ✗ written back: UTF-8 re-encodes high bytes\n}\n\n# Fix — re-binarify before writing back (or avoid the string detour):\nwhen HTTP_REQUEST_DATA {\n    set body [HTTP::payload]\n    set body \"$body INJECTED\"\n    binary scan $body c* -             ;# forces a byte-array intrep\n    HTTP::payload replace 0 100 $body  ;# ✓ written byte-for-byte\n}\n\n# Plain Tcl — string case folding mangles a byte array directly:\nset ba [binary format c* {128 195 255}]\nset up [string toupper $ba]            ;# ✗ S110: 0xFF → U+0178 corrupts the bytes\n```\n\nColour-aware data provenance tracking follows untrusted I/O through\nassignments, interpolation, and phi nodes to dangerous sinks. Commands that\nproduce fixed-type results (e.g. `string length`\n\n) act as sanitisers.\n\n```\n# T100 — tainted data in dangerous sink:\nset input [gets stdin]\neval $input                  ;# ✗ tainted data flows into eval\n\n# T102 — tainted data in option position:\nset pat [HTTP::uri]\nregexp $pat $string          ;# ✗ tainted pattern without '--' terminator\nregexp -- $pat $string       ;# ✓ safe: '--' prevents option injection\n\n# IRULE1007 — collect without release (side-aware):\nwhen HTTP_REQUEST {\n    HTTP::collect 1048576    ;# ✗ missing matching HTTP::release on client side\n}\n```\n\nCall graph, symbol graph, and data-flow graph are exposed for AI agent consumption — enabling automated code review, impact analysis, and refactoring assistance.\n\n```\nproc validate {input} { string is integer $input }\nproc store {data}     { puts $data }\nproc process {x}      { if {[validate $x]} { store $x } }\n\n# Call graph query: \"who calls validate?\" → process\n# Symbol graph query: \"variables in process\" → x\n# Data-flow query: \"trace $x\" → parameter → validate → store\n```\n\nAn interactive webview panel (Ctrl+Alt+E / Cmd+Alt+E) that visualises the\ncompiler's intermediate representation, control-flow graph, SSA form,\noptimiser output, Tcl bytecode, and WebAssembly disassembly for the active\neditor. The **WASM** tab renders each instruction with its originating Tcl\nsource range (click an instruction to place the source cursor inside the\nexpression, substituted command, or post-`;`\n\nsub-command it compiled from),\nresolved call targets (click `call 42 ; ::greet`\n\nto jump to both the\ncallee's disassembly and its definition), resolved branch targets (click\n`br 0 ; loop_header foreach`\n\nto jump to the matching `loop`\n\nopen), a\nlabelled `block`\n\n/ `loop`\n\n/ `if`\n\nfor each Tcl construct (`foreach`\n\n,\n`while`\n\n, `for`\n\n, `if`\n\n, `catch body`\n\n, `switch arm`\n\n), a source-line comment\nabove every instruction group, and orthogonal control-flow arrows in the\nleft gutter.\n\nThe IR, CFG, SSA, bytecode, and WASM tabs each carry an **optimiser lens**\n(`off`\n\n/ `on`\n\n/ `diff`\n\n). The `diff`\n\nmode compares the relevant node — IR\nstatement, CFG block, or bytecode instruction — rather than raw text, so\nbyte offsets, source ranges, sequence indices, and tree-connector glyphs\nthat merely shift when the optimiser adds or removes a node are ignored.\nA single rewrite then shows as a single localised change instead of every\nfollowing line being flagged. The `tcl-explorer`\n\nCLI and TUI render the\nsame offset-free diff via `--opt diff`\n\n.\n\n```\n┌─────────────────────────────────────────────────┐\n│  IR  │  CFG  │  SSA  │  Optimiser  │  Bytecode  │\n├─────────────────────────────────────────────────┤\n│  proc fibonacci {n}                             │\n│    ENTRY:                                       │\n│      %0 = param n                               │\n│      %1 = const 0        ;  set a 0             │\n│      %2 = const 1        ;  set b 1             │\n│    LOOP:                                        │\n│      %3 = phi [%1, ENTRY] [%6, BODY]            │\n│      ...                                        │\n└─────────────────────────────────────────────────┘\n```\n\nA live preview panel that extracts the widget hierarchy from Tk source and renders a visual layout — updates in real time as you edit.\n\n```\npackage require Tk\nttk::frame .f\nttk::label .f.lbl -text \"Name:\"\nttk::entry .f.ent -textvariable name\nttk::button .f.btn -text \"OK\" -command { puts $name }\ngrid .f.lbl .f.ent .f.btn -padx 5 -pady 5\npack .f\n# Preview panel shows the grid layout with label, entry, and button\n```\n\nOpen a BIG-IP `.conf`\n\nor `.scf`\n\nfile to get syntax highlighting, object\nnavigation, and iRule extraction.\n\n```\n# BIG-IP config file (bigip.conf)\nltm virtual /Common/my_vs {\n    destination /Common/10.0.0.1:443\n    pool /Common/my_pool\n    rules {\n        /Common/my_irule        ← right-click → \"Open iRule in Editor\"\n    }\n}\n# \"Extract All iRules to Files...\" exports every iRule to separate .tcl files\n```\n\n** f5 CLI tool with a cleanup verb** — find every object the\nconfiguration defines but no virtual server (or wide-IP) references,\nand emit a\n\n`tmsh delete`\n\nscript in reverse-topological order so each\ndelete runs only after the objects that reference its target have\nalready been removed. iRule bodies are scanned too (`pool …`\n\n,\n`SSL::profile …`\n\n, `class match …`\n\n, `persist …`\n\n, `snatpool …`\n\n,\n`virtual …`\n\n, `node …`\n\n, `LSN::pool …`\n\n, `STATS::*`\n\n, `ifile …`\n\n,\n`HTTP::respond ifile …`\n\n, plus every other iRule command that names a\nBIG-IP object). Constant-string variables are tracked through `set var /Common/foo; pool $var`\n\nlinear copy-propagation, so refs written\nthrough local bindings are caught.\n\n```\nf5 cleanup samples/bigip/bigip.conf\nf5 cleanup --keep /Common/critical_pool bigip.conf\nf5 cleanup --json bigip.conf > report.json\n```\n\n** f5 grep verb** — find every BIG-IP object related to a given\nobject name (or regex, or CIDR) by walking the same\nforward-and-reverse reference graph the cleanup analysis uses. By\ndefault the BFS traverses both directions, so a single command\nsurfaces the seed's full neighbourhood: forward edges (objects the\nseed depends on) and reverse edges (objects that depend on the seed).\n\n`--cidr`\n\nswitches the seed selector from \"match the object's full\npath\" to \"match an IP address or CIDR mentioned anywhere inside the\nobject — header, body, or iRule script\". Multiple networks may be\npassed at once as a comma- or whitespace-separated list, and an\nobject qualifies when any IP/CIDR token in its text overlaps any\nrequested network. This catches addresses buried deep inside iRule\nbodies (`if { [IP::addr [IP::client_addr] equals \"10.0.0.5\"] }`\n\n,\n`class match … \"10.0.0.0/8\"`\n\n, …) that a plain path grep can't reach.\n\n```\nf5 grep /Common/web_pool bigip.conf\nf5 grep --direction reverse /Common/web1 bigip.conf\nf5 grep --regex '^/Common/(web|api)_pool$' bigip.conf\nf5 grep --json --max-depth 2 web_pool bigip.conf\nf5 grep --cidr 10.0.0.0/8 bigip.conf\nf5 grep --cidr '10.0.0.0/8, 192.168.0.0/16' bigip.conf\nf5 grep --no-recurse --cidr 10.0.0.0/8 bigip.conf\n```\n\nThe related-object BFS is on by default; pass `--no-recurse`\n\nto\nskip it and return only the objects that directly match the\npattern (`-r`\n\n/ `--recurse`\n\ntoggle it explicitly back on). This\napplies to every match mode: substring, `--regex`\n\n, and `--cidr`\n\n.\n\n** f5 irule verb group** — iRules-specific analysis with\n\n`event-order`\n\nand `event-info`\n\nsub-actions, defaulting to the\n`f5-irules`\n\ndialect:\n\n```\nf5 irule event-order samples/irules/policy.irule\nf5 irule event-info HTTP_REQUEST --json\n```\n\n`f5`\n\nis a separate CLI from `tcl`\n\n. The full verb list (today):\n\n| Group | Verbs |\n|---|---|\n| Acquisition | `fetch` , `extract` (UCS → SCF) |\n| Analysis | `stats` , `graph` , `explain` , `diff` , `grep` , `cleanup` , `validate` |\n| Transformation | `rename` , `redact` , `unredact` , `encrypt-secrets` , `decrypt-secrets` , `pcap-remap` , `split` , `merge` , `convert` , `tmsh` |\n| Round-trip | `pull` , `push` |\n| iRules | `irule event-order` , `irule event-info` , `irule lint` , `irule trace` , `irule extract` |\n| Misc | `completion` |\n\nHighlights of the newer verbs:\n\n-\n— pull SCF/UCS from a live BIG-IP via iControl REST or SSH (system`f5 fetch`\n\n`ssh`\n\n/`scp`\n\n). Credentials resolve from CLI flags, env vars, an XDG`hosts.toml`\n\n, or interactive prompt. -\n**Encrypted UCS**— archives saved with`tmsh save sys ucs <name> passphrase <pass>`\n\nare GnuPG symmetric (AES-128) OpenPGP messages (F5 KB K5437). Every verb that reads a`.ucs`\n\n—`extract`\n\n,`convert ucs2scf`\n\n,`query`\n\n,`grep`\n\n,`cleanup`\n\n,`diff`\n\n,`irule …`\n\n— decrypts them transparently and entirely**in memory**; the decrypted archive (which holds SSL private keys) never touches disk. The passphrase is read from`$F5_UCS_PASSPHRASE`\n\nor a secure terminal prompt;`extract`\n\nand`convert`\n\nalso accept`--passphrase-env VAR`\n\n/`--no-passphrase-prompt`\n\n. Decryption shells out to`gpg`\n\n/`gpg2`\n\nwhen present (exactly what BIG-IP uses) and otherwise falls back to a bundled, dependency-free pure-Python OpenPGP decryptor, so it works even in the zipapp on a host with no GnuPG installed.\n\n```\nexport F5_UCS_PASSPHRASE='…'        # or be prompted on a TTY\nf5 extract encrypted.ucs -o prod.scf\nf5 query '.ltm.virtual[].name' encrypted.ucs\n```\n\n-\n— print the resolved profile chain, iRule chain, persistence, SNAT, default pool, and members for one object: the operator's \"what actually happens to this VIP?\" question, answered in one command.`f5 explain {virtual|pool} <name>`\n\n-\n— semantic, object-aware diff that ignores property ordering and iRule whitespace. Each side may be an SCF /`f5 diff old.scf new.scf`\n\n`bigip.conf`\n\nstanza dump*or*a tmsh command script (`tmsh create`\n\n/`tmsh modify`\n\nlines, as emitted by`f5 tmsh`\n\nor pasted from a BIG-IP shell), and the two formats may be mixed. Every config-producing verb (`extract`\n\n,`pull`\n\n,`grep`\n\n,`split`\n\n,`merge`\n\n,`rename`\n\n,`redact`\n\n,`unredact`\n\n) also takes`--format scf|tmsh`\n\nso the same artefact can be replayed either way. -\n— strip secrets and remap public IPs while preserving CIDR relationships (a /24 of real IPs lands in a /24 of redacted IPs). A sidecar map file makes the redaction reversible`f5 redact`\n\n+`f5 unredact`\n\n*and stable across runs*— re-running`redact`\n\nwith the same map reuses every prior assignment, so iterative work with F5 support stays consistent.`unredact`\n\nwalks the map in reverse over any text, including support emails and log snippets. -\n— encrypt or decrypt the credential-bearing values in a`f5 encrypt-secrets`\n\n+`f5 decrypt-secrets`\n\n`bigip.conf`\n\n/ SCF (passphrase, password, secret, shared-secret, auth-password, privacy-password) using the unit master key — the base64 key`f5mku -K`\n\nprints on the device.`encrypt-secrets`\n\nwraps clear-text values in the`$M$<salt>$<base64>`\n\nenvelope BIG-IP stores;`decrypt-secrets`\n\nrecovers the clear text. Both leave values already in the target form untouched, so they are idempotent. The key is supplied via`--f5mku KEY`\n\n,`--f5mku-file FILE`\n\n, or`$F5MKU`\n\n, otherwise it is read from a secure`F5 MKU Key:`\n\nterminal prompt (suppress with`--no-key-prompt`\n\n); the AES-ECB transform runs on the bundled pure-Python cipher, so it works in the zipapp with no`cryptography`\n\ndependency.\n\n```\nf5mku -K > key.txt                                       # on the device\nf5 decrypt-secrets bigip.conf --f5mku-file key.txt       # reveal secrets\nF5MKU=\"$(cat key.txt)\" f5 encrypt-secrets clear.conf -o sealed.conf\n```\n\n-\n— apply the same map to a PCAP capture: rewrites IPv4/IPv6 src/dst, recomputes IP and TCP/UDP/ICMP checksums, and`f5 pcap-remap`\n\n*parses*the F5 Ethernet trailer (legacy + DPT formats;`tcpdump -i 0.0:nnnp`\n\n) to rewrite peer IPs at schema-known offsets. Schema ported from Wireshark's`packet-f5ethtrailer.c`\n\n;`--schema FILE`\n\nlayers in fleet-specific extensions;`--on-unknown=error|preserve|sweep`\n\npicks the policy when a TLV has no registered layout. -\n— emit`f5 tmsh`\n\n`tmsh create`\n\n(or`--modify`\n\n) commands for every object in a config, in dependency order so the script can be pasted into a BIG-IP shell unchanged. -\n— small jq-flavoured DSL for inspecting and rewriting BIG-IP configs. Built-in`f5 query`\n\n(alias`f5 q`\n\n)**renderer plugins** turn query output into a Mermaid diagram, an ASCII Gantt timeline of monitor up/down transitions, or a Unicode line-art block diagram — no sidecar Python scripts required. Run`f5 q --help-renderers`\n\nfor the catalogue:\n\n```\n# ASCII Gantt of pool-member up/down events from a BIG-IP log\nf5 q --render gantt '\n    f5log_load(\"ltm.log\")[]\n    | select(.module == \"01340011\" or .module == \"01340012\")\n    | tsv(.timestamp,\n          (sub(.message, \"^.*member \", \"\") | sub(., \" monitor.*$\", \"\")),\n          (if .module == \"01340011\" then \"DOWN\" else \"UP\" end))\n' bigip.conf\n\n# Mermaid diagram of every web virtual server and its references\nf5 q --render mermaid '.ltm.virtual[\"~/web_\"]' bigip.conf\n```\n\n**Use the query engine from Python** — the same engine is importable\nas `f5q`\n\nso external scripts can drive queries, build them up\nprogressively, render results through plugins or inline callables,\nand ship reusable extensions via one-line decorators:\n\n``` python\nfrom f5q import q, load, renderer, builtin, input_format\n\n# One-liner: q() takes (expression, *inputs).\nfor name in q(\".ltm.virtual[] | .name\", \"bigip.conf\"):\n    print(name)\n\n# Progressive — chain queries on top of each other (typed wrapper, immutable).\nfiltered = q(\".ltm.virtual[]\", \"bigip.conf\").q(\".[] | select(.pool != null)\").q(\".[] | .name\")\n\n# Render via a registered plugin OR an inline callable.\nfiltered.render(\"ascii-blocks\")\nfiltered.render(lambda values, **opts: \", \".join(map(str, values)) + \"\\n\")\n\n# Coerce to plain JSON-friendly Python.\ndata = filtered.out()  # [{\"kind\": ..., \"fields\": {...}}, ...]\n\n# Pre-stage once, query many times. Custom file formats? Pass an inline parser.\ncorpus = load(\"ltm.conf\", \"gtm.conf\")\nroutes = load(\"routes.xml\", parser=my_xml_parser)\n\n# Ship a custom renderer the f5 CLI can dispatch via --render NAME.\n@renderer(\"md-table\", summary=\"Markdown table of results.\", accepts=\"any\")\ndef _render(values, **opts):\n    return \"| name |\\n| ---- |\\n\" + \"\\n\".join(f\"| {v} |\" for v in values)\n\n# Ship a custom DSL function the query language can call.\n@builtin(\"uppercase\", summary=\"ASCII uppercase.\", min_args=1, max_args=1)\ndef _u(s):\n    return str(s).upper()\n\n# Ship a custom side-input format `--input KIND NAME=PATH` can load.\n@input_format(\"yaml\", summary=\"YAML side-input.\")\ndef _parse_yaml(source, *, uri, options=()):\n    import yaml\n\n    return yaml.safe_load(source)\n```\n\n**Auto-discovered plugins** — drop any of the above into\n`$XDG_CONFIG_HOME/dialects/f5/query/plugins/*.py`\n\n(default\n`~/.config/dialects/f5/query/plugins/*.py`\n\n) and the engine picks them up on the\nfirst registry access, no import dance required. Broken plugins\nwarn to stderr and are skipped; `f5 q --help-plugins`\n\nshows what\nloaded.\n\n**Documentation**:\n\n**Python API reference**— autodoc-generated, every public symbol with full signature, docstring, and`[source]`\n\nlinks. Build locally with`make docs-html`\n\n(output at`docs/sphinx/_build/html/index.html`\n\n); the same Sphinx tree builds on Read the Docs via.`.readthedocs.yaml`\n\n[KCS: how-to — script against](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-howto-script-against-f5-query-from-python.md)— task-oriented walkthrough.`f5 query`\n\nfrom Python[KCS: feature —](/bitwisecook/tcl-lsp/blob/main/docs/kcs/features/kcs-feature-f5-query-renderers.md)— built-in plugin catalogue and CLI flag reference.`f5 query`\n\nplugins[Design —](/bitwisecook/tcl-lsp/blob/main/docs/design/f5-query-renderer-contract.md)— formal contracts, registration lifecycle, error mapping.`f5 query`\n\nplugin contract\n\n**Install the f5 CLI** — the released artefact is a single-file\nzipapp (\n\n`f5-<version>.pyz`\n\n) that needs only Python 3.10+ on the host.\nSee [INSTALL-cli.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-cli.md)for the one-line\n\n`curl | sh`\n\ninstaller, manual install steps for macOS/Debian/Ubuntu/RHEL/CentOS/\nFedora, shell completion setup, and source-build instructions.In VS Code, run the command palette entry **Tcl: Generate BIG-IP\nCleanup Script** while a `bigip.conf`\n\nis open; the script and its JSON\nmetadata report open side-by-side. See\n[KCS: feature — BIG-IP Config Cleanup](/bitwisecook/tcl-lsp/blob/main/docs/kcs/features/kcs-feature-bigip-cleanup.md)\nfor the full options reference.\n\nOpen `.apl`\n\nfiles or files named `presentation`\n\nto get semantic highlighting\nfor the iApp Application Presentation Language. APL-specific tokens include\nsection/table/row keywords, field types (`string`\n\n, `choice`\n\n, `password`\n\n, ...),\nattributes (`default`\n\n, `display`\n\n, `required`\n\n, `validator`\n\n), `define`\n\nblocks,\n`optional`\n\nconditionals, `#include`\n\n/`#inline`\n\ndirectives, and validator names.\nEmbedded Tcl inside `[...]`\n\nbrackets (e.g. `[tmsh::get_config ...]`\n\n) receives\nfull Tcl semantic highlighting.\n\n```\n# iApp APL presentation file\nsection basic {\n    string addr default \"0.0.0.0\" required validator \"IpAddress\"\n    choice protocol display \"medium\" default \"tcp\" {\n        \"TCP\" => \"tcp\",\n        \"UDP\" => \"udp\"\n    }\n    yesno use_snat default \"yes\"\n}\ntext {\n    basic \"Basic Configuration\"\n    basic.addr \"Virtual Server IP Address\"\n}\n```\n\n**Cross-file integration:** When a `presentation`\n\n(APL) file and an\n`implementation`\n\n(iApp Tcl) file are in the same directory, the server\ncross-validates them:\n\n**IAPP7001**: Implementation references a variable (`$::section__field`\n\n) not defined in the presentation**IAPP7002**: Presentation field is never referenced in the implementation** IAPP7003**:`#include`\n\nfile not found\n\nThe `#include`\n\ndirective is resolved relative to the APL file's directory,\nwith recursive resolution and circular-include protection.\n\nThe `f5-iapps`\n\ndialect includes 30+ `tmsh::`\n\nnamespace commands\n(`tmsh::create`\n\n, `tmsh::modify`\n\n, `tmsh::get_config`\n\n, `tmsh::get_field_value`\n\n,\netc.) and 4 `script::`\n\ncommands (`script::run`\n\n, `script::init`\n\n, etc.) with\nhover documentation and arity validation.\n\nTranslate F5 BIG-IP iRules to F5 Distributed Cloud configuration, with both Terraform HCL and JSON API output plus a coverage report.\n\n```\n# Source iRule:\nwhen HTTP_REQUEST {\n    if { [HTTP::uri] starts_with \"/api\" } {\n        pool api_pool\n    } else {\n        HTTP::redirect \"https://[HTTP::host]/api[HTTP::uri]\"\n    }\n}\n\n# \"Translate iRule to F5 XC\" produces:\n# - Terraform HCL with routes, origin pools, and redirect rules\n# - JSON API payload for direct XC API calls\n# - Coverage report showing translated vs. untranslatable constructs\n```\n\nGenerate and run deterministic tests for F5 iRules. The framework simulates\nBIG-IP's event lifecycle, pool selection, data groups, and multi-TMM CMP\nbehaviour in a standard `tclsh`\n\n.\n\n```\n::orch::configure_tests \\\n    -profiles {TCP HTTP} \\\n    -irule { when HTTP_REQUEST { pool web_pool } } \\\n    -setup { ::orch::add_pool web_pool {{10.0.0.1:80}} }\n\n::orch::test \"routing-1.0\" \"basic request goes to web_pool\" -body {\n    ::orch::run_http_request -host \"example.com\" -uri \"/\"\n    ::orch::assert_that pool_selected equals \"web_pool\"\n}\n\nexit [::orch::done]\n```\n\nThe `generate-test`\n\nCLI command and `generate_irule_test`\n\nMCP tool analyse an\niRule's control-flow graph to produce test cases automatically. For iRules\nwith CMP-sensitive patterns (`static::`\n\nwrites in hot events, `table`\n\nshared\nstate), multi-TMM scenarios using fakeCMP distribution are included.\n\nOptionally run the active file through a real `tclsh`\n\n(or an iRules stub\nadapter) on save to catch issues that static analysis alone cannot detect.\n\n```\n# With tclLsp.runtimeValidation.enabled = true:\nproc test {} {\n    package require NoSuchPackage   ;# runtime error: can't find package\n}\n# The server invokes tclsh in syntax-check mode and merges runtime\n# errors into the diagnostics panel alongside static analysis results\n```\n\nEditor commands for common encoding operations, available from the right-click context menu or the command palette.\n\n```\nEscape Selection          →  converts special chars to Tcl backslash sequences\nUnescape Selection        →  reverses backslash sequences to literal chars\nBase64 Encode Selection   →  encodes selected text as base64\nBase64 Decode Selection   →  decodes base64 back to text\nCopy File as Base64       →  copies entire file content as base64 to clipboard\nCopy File as Gzip+Base64  →  compresses then base64-encodes file to clipboard\n```\n\nGenerate a complete Tcl package project layout with a single command.\n\n```\n\"Tcl: Scaffold Tcl Package Starter\" creates:\n\n  mypackage/\n    pkgIndex.tcl          Package index\n    mypackage.tcl         Package source with namespace and public API\n    tests/\n      all.tcl             Test runner\n      mypackage.test      tcltest skeleton\n    .github/\n      workflows/ci.yml    GitHub Actions CI workflow\n    README.md             Package README\n```\n\nThree chat participants integrate with GitHub Copilot to provide domain-specific AI assistance backed by the LSP's static analysis.\n\n| Command | Description |\n|---|---|\n`/create` |\nGenerate a new iRule from a natural-language description |\n`/explain` |\nExplain what an iRule does, including data flow and security |\n`/fix` |\nIteratively fix all LSP diagnostics in the current iRule |\n`/validate` |\nRun full LSP validation and show a categorised report |\n`/review` |\nDeep security and safety review (injection, DoS, races) |\n`/find-legacy` |\nFind and modernise legacy patterns (unbraced expr, matchclass, etc.) |\n`/optimise` |\nApply optimiser suggestions with explanations |\n`/scaffold` |\nGenerate an iRule skeleton from selected events |\n`/datagroup` |\nSuggest data-group extraction for inline lookups |\n`/diff` |\nExplain differences between two iRule versions |\n`/event` |\nShow which commands are valid in a given event |\n`/migrate` |\nConvert nginx/Apache/HAProxy config to an iRule |\n`/diagram` |\nGenerate a Mermaid flowchart of the iRule's logic flow |\n`/xc` |\nTranslate the iRule to F5 Distributed Cloud configuration |\n\n```\nUser:   @irule /create rate limiter that allows 100 requests per minute per client IP\nCopilot: generates a complete iRule with HTTP_REQUEST handler, table-based\n         counting, and HTTP::respond 429 — validated against the LSP\n```\n\n| Command | Description |\n|---|---|\n`/create` |\nGenerate Tcl code from a description |\n`/explain` |\nExplain what Tcl code does |\n`/fix` |\nIteratively fix all LSP diagnostics |\n`/validate` |\nRun full LSP validation and show a report |\n`/optimise` |\nApply optimiser suggestions with explanations |\n\n```\nUser:   @tcl /explain what does the fibonacci proc do?\nCopilot: walks through the loop, variable assignments, and return value\n```\n\n| Command | Description |\n|---|---|\n`/create` |\nGenerate a Tk GUI from a description |\n`/explain` |\nExplain the widget hierarchy and layout |\n`/preview` |\nOpen the Tk Preview pane for the current file |\n\n```\nUser:   @tk /create a simple calculator with number buttons and a display\nCopilot: generates Tk code with grid layout, button callbacks, and display label\n```\n\nTwenty purpose-built skills for Claude Code (CLI) that combine LSP static\nanalysis with AI reasoning. Each skill invokes the `tcl-lsp-ai`\n\nanalyser,\niterates on diagnostics, and produces clean output.\n\n| Skill | Description |\n|---|---|\n`irule-create` |\nGenerate a new iRule from a description, validate until clean |\n`irule-explain` |\nExplain an iRule's logic, data flow, and security posture |\n`irule-fix` |\nIteratively fix all diagnostics (analyse → fix → re-analyse) |\n`irule-validate` |\nCategorised validation report (errors, security, style, optimiser) |\n`irule-review` |\nDeep security audit: injection, DoS, races, information leakage |\n`irule-convert` |\nModernise legacy patterns to current best practices |\n`irule-optimise` |\nApply optimiser suggestions with safety explanations |\n`irule-scaffold` |\nGenerate event skeleton with log gating and placeholders |\n`irule-datagroup` |\nSuggest data-group extraction for inline lookups |\n`irule-diff` |\nExplain semantic differences between two iRule versions |\n`irule-event` |\nLook up event/command validity from the registry |\n`irule-migrate` |\nConvert nginx/Apache/HAProxy config to an iRule |\n`irule-diagram` |\nGenerate a Mermaid flowchart from compiler IR |\n`irule-xc` |\nTranslate to F5 XC with Terraform and JSON output |\n`tcl-create` |\nGenerate Tcl code from a description, validate until clean |\n`tcl-explain` |\nExplain Tcl code with analysis context |\n`tcl-fix` |\nIteratively fix all Tcl diagnostics |\n`tcl-validate` |\nCategorised Tcl validation report |\n`tcl-optimise` |\nApply Tcl optimiser suggestions |\n`tk-create` |\nGenerate Tk GUI code with proper widget hierarchy |\n\n```\n# Example: fix all issues in an iRule\nclaude /irule-fix my_irule.tcl\n\n# Example: security review\nclaude /irule-review production_rule.tcl\n\n# Example: generate a Mermaid diagram\nclaude /irule-diagram complex_rule.tcl\n```\n\nA Model Context Protocol server that exposes tcl-lsp analysis as 27 tools for any MCP-compatible client (Claude Desktop, custom agents, etc.).\n\n| Tool | Description |\n|---|---|\n`analyze` |\nFull analysis: diagnostics, symbols, events, and metadata |\n`validate` |\nCategorised validation report |\n`review` |\nSecurity-focused diagnostic report |\n`find-legacy` |\nDetect legacy patterns eligible for modernisation |\n`optimize` |\nOptimisation suggestions with rewritten source |\n`hover` |\nHover information at a position |\n`complete` |\nCompletions at a position |\n`goto_definition` |\nFind definition of a symbol |\n`find_references` |\nFind all references to a symbol |\n`symbols` |\nDocument symbol hierarchy |\n`code_actions` |\nQuick fixes for a source range |\n`format_source` |\nFormat Tcl/iRules source code |\n`rename` |\nRename a symbol throughout the document |\n`event_info` |\niRules event metadata and valid commands |\n`command_info` |\nCommand metadata and valid events |\n`event_order` |\nEvents in canonical firing order |\n`call_graph` |\nBuild proc call graph with roots and leaves |\n`symbol_graph` |\nBuild scope/definition/reference graph |\n`dataflow_graph` |\nBuild taint and side-effect graph |\n`diagram` |\nExtract control-flow diagram data from IR |\n`xc_translate` |\nTranslate iRule to XC configuration |\n`tk_layout` |\nExtract Tk widget tree as JSON |\n`generate_irule_test` |\nGenerate iRule test script with CFG paths and multi-TMM detection |\n`irule_cfg_paths` |\nExtract CFG control-flow paths for test planning |\n`fakecmp_which_tmm` |\nLook up which TMM a connection tuple maps to |\n`fakecmp_suggest_sources` |\nFind client addr/port combos that hit each TMM |\n`set_dialect` |\nSet active Tcl dialect for the session |\n\n```\n// Claude Desktop — claude_desktop_config.json\n{\n  \"mcpServers\": {\n    \"tcl-lsp\": {\n      \"command\": \"./tcl-lsp-mcp-server.pyz\"\n    }\n  }\n}\n```\n\n`tcl pkg`\n\nis a deterministic Tcl package manager using Go-style Minimum\nVersion Selection and a content-addressable SHA-256 cache. `tcl venv`\n\ncreates\nisolated virtual environments that pin a specific tclsh version.\n\n```\n# Quick start\ntcl venv create .venv            # create a virtual environment\nsource .venv/bin/activate        # activate it\ntcl pkg init                     # create tclpkg.tcl manifest\ntcl pkg add json 1.0             # add a dependency\ntcl pkg install                  # resolve, fetch, and lock\ntcl pkg tree                     # show dependency tree\ntcl pkg verify                   # check integrity hashes\n```\n\nThe manifest is a native Tcl file (`tclpkg.tcl`\n\n) evaluated in a sandboxed\ninterpreter. The lockfile (`tclpkg.lock`\n\n) is canonical JSON — two runs against\nthe same manifest produce byte-identical output (aside from the\n`generated`\n\ntimestamp, which `--frozen`\n\npreserves).\n\n```\n# tclpkg.tcl — example manifest\npackage     myapp\nversion     1.0.0\nlicense     MIT\ntcl         >=8.6\n\nrequire json    1.3.5\nrequire http    2.9.8\ndev-require tcltest 2.5.5\n```\n\nThe LSP server auto-detects `tclpkg.tcl`\n\nprojects and venv `lib/`\n\ndirectories,\nand offers an \"Install via tclpkg\" quick-fix on missing-package diagnostics.\n\nSee [docs/kcs/kcs-tclpkg-overview.md](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-tclpkg-overview.md) for the\nfull architecture and contracts.\n\nAll CLI tools are distributed as self-contained Python zipapps (`.pyz`\n\n) — no\n`pip install`\n\nrequired.\n\nA single verb-based CLI that aggregates common local workflows:\n\n`opt`\n\n/`optimise`\n\n— optimise combined input source and emit rewritten Tcl`diag`\n\n— run diagnostics across files/directories/packages`lint`\n\n— run lint diagnostics across files/directories/packages`validate`\n\n— error-level validation checks`format`\n\n— format source using canonical Tcl style rules`symbols`\n\n— emit symbol definitions for the resolved source`diagram`\n\n— extract control-flow diagram data from compiler IR`callgraph`\n\n— build procedure call graph data`symbolgraph`\n\n— build symbol relationship graph data`dataflow`\n\n— build taint/effect data-flow graph data`command-info`\n\n— look up command registry metadata`find-legacy`\n\n— detect legacy modernisation patterns (detection only)`dis`\n\n— bytecode disassembly`compwasm`\n\n— compile input to a WASM binary`highlight`\n\n— emit syntax-highlighted source (`ansi`\n\nor`html`\n\n)`diff`\n\n— compare two sources across AST/IR/CFG compiler representations`explore`\n\n— run compiler-explorer views (`ir`\n\n,`cfg`\n\n,`ssa`\n\n,`opt`\n\n,`asm`\n\n,`wasm`\n\n, ...)`help`\n\n— search bundled KCS feature docs from the SQLite help index`pkg`\n\n— package management:`init`\n\n,`add`\n\n,`remove`\n\n,`install`\n\n,`list`\n\n,`tree`\n\n,`verify`\n\n,`info`\n\n,`search`\n\n,`update`\n\n,`sync`\n\n,`outdated`\n\n,`why`\n\n,`vendor`\n\n,`run`\n\n`venv`\n\n— virtual environments:`create`\n\n,`delete`\n\n,`info`\n\n,`activate`\n\n,`deactivate`\n\n,`list`\n\n,`update`\n\n,`run`\n\n```\n# Optimise everything under src/ into one output script\npython tcl.pyz opt src/ -o build/optimised.tcl\n\n# Run diagnostics across a directory and a Tcl package\npython tcl.pyz diag src/ mypkg --package-path ./vendor/tcl\n\n# Run lint diagnostics (same checks as `diag`)\npython tcl.pyz lint src/ mypkg --package-path ./vendor/tcl\n\n# Validate syntax/error diagnostics\npython tcl.pyz validate src/\n\n# Validate as JSON\npython tcl.pyz validate src/ --json\n\n# Format source text\npython tcl.pyz format script.tcl -o formatted.tcl\n\n# Minify source (strip comments, collapse whitespace, join commands)\npython tcl.pyz minify script.tcl -o minified.tcl\n\n# Aggressive minify (optimise + static substring folding via SCCP + name compaction)\npython tcl.pyz minify --aggressive script.tcl -o minified.tcl --symbol-map map.txt\n\n# Symbol/graph/find-legacy analysis verbs\npython tcl.pyz symbols script.tcl --json\npython tcl.pyz diagram script.tcl --json\npython tcl.pyz callgraph script.tcl --json\npython tcl.pyz symbolgraph script.tcl --json\npython tcl.pyz dataflow script.tcl --json\npython tcl.pyz command-info HTTP::uri --dialect f5-irules --json\npython tcl.pyz find-legacy rule.irule --json\n\n# iRules-specific lookups live on the f5 CLI:\npython f5.pyz irule event-order rule.irule --json\npython f5.pyz irule event-info HTTP_REQUEST --json\n\n# Emit bytecode disassembly\npython tcl.pyz dis script.tcl\n\n# Compile to WASM binary (+ optional WAT sidecar)\npython tcl.pyz compwasm script.tcl -o out.wasm --wat-output out.wat\n\n# Emit ANSI-highlighted output (or --format html)\npython tcl.pyz highlight script.tcl --force-colour\n\n# Diff two iRules using compiler structure layers\npython tcl.pyz diff old.irule new.irule --show ast,ir,cfg\n\n# Use compiler explorer views from the same zipapp\npython tcl.pyz explore script.tcl --show ir,cfg,opt\n\n# Search KCS help docs (optionally scoped by dialect)\npython tcl.pyz help taint analysis --dialect f5-irules\n\n# Show help for the help command itself\npython tcl.pyz help --help\n\n# Emit help search results as JSON\npython tcl.pyz help taint --json\n```\n\nFor iRules input, pass `--dialect f5-irules`\n\nexplicitly:\n\n```\ntcl.pyz lint rules/ --dialect f5-irules\n```\n\niRules-specific verbs (`event-order`\n\n, `event-info`\n\n) live on the separate\n`f5`\n\nCLI under the `irule`\n\nverb group — see the F5 BIG-IP CLI section.\n\nFor source builds, run `make kcs-db`\n\nbefore packaging zipapps so `tcl.pyz help`\n\ncan query the bundled KCS SQLite database.\n\n**Install the tcl CLI** — the released artefact is a single-file\nzipapp (\n\n`tcl-<version>.pyz`\n\n) that needs only Python 3.10+ on the host.\nSee [INSTALL-cli.md](/bitwisecook/tcl-lsp/blob/main/INSTALL-cli.md)for the one-line\n\n`curl | sh`\n\ninstaller, manual install steps for macOS/Debian/Ubuntu/RHEL/CentOS/\nFedora, source builds, and shell completion (`bash`\n\n, `zsh`\n\n, `fish`\n\n)\nthat covers every verb, dialect, optimiser profile, and source-path\nglob (`*.tcl`\n\n, `*.tk`\n\n, `*.itcl`\n\n, `*.tm`\n\n, `*.irul`\n\n, `*.irule`\n\n,\n`*.iapp`\n\n, `*.iappimpl`\n\n).Console tool for inspecting the compiler pipeline: IR, CFG, SSA, optimiser rewrites, shimmer warnings, taint analysis, and bytecode.\n\n```\n# Full exploration of a Tcl file\nuv run python -m tooling.explorer script.tcl\n\n# Focus on optimiser rewrites only\nuv run python -m tooling.explorer script.tcl --show opt\n\n# Inline source with optimised output\nuv run python -m tooling.explorer --source 'set a 1; set b [expr {$a + 2}]' --show-optimised-source\n\n# Show only IR and CFG\nuv run python -m tooling.explorer script.tcl --show ir,cfg\n\n# iRules dialect with flow analysis\nuv run python -m tooling.explorer irule.tcl --dialect bigip --show irules\n```\n\nAvailable views: `ir`\n\n, `cfg`\n\n, `ssa`\n\n, `interproc`\n\n, `types`\n\n, `opt`\n\n, `gvn`\n\n,\n`shimmer`\n\n, `taint`\n\n, `irules`\n\n, `callouts`\n\n, `asm`\n\n, `wasm`\n\n. Groups: `all`\n\n,\n`compiler`\n\n, `optimiser`\n\n.\n\nStandalone static analyser for use with AI agents and CI pipelines.\n\n```\n# Full context pack (diagnostics + symbols + events) as JSON\nuv run python -m ai.claude.tcl_ai context script.tcl\n\n# Categorised validation report\nuv run python -m ai.claude.tcl_ai validate script.tcl\n\n# Security-focused review\nuv run python -m ai.claude.tcl_ai review irule.tcl\n\n# Optimisation suggestions with rewritten source\nuv run python -m ai.claude.tcl_ai optimize script.tcl\n\n# Build call graph\nuv run python -m ai.claude.tcl_ai call-graph script.tcl\n\n# Look up iRules event metadata\nuv run python -m ai.claude.tcl_ai event-info HTTP_REQUEST\n\n# Extract Tk widget tree\nuv run python -m ai.claude.tcl_ai tk-layout gui.tcl\n\n# Generate iRule test script (Event Orchestrator framework)\nuv run python -m ai.claude.tcl_ai generate-test irule.tcl\n\n# Extract CFG paths for test planning\nuv run python -m ai.claude.tcl_ai cfg-paths irule.tcl\n```\n\nCompile Tcl scripts to WebAssembly (WAT text or binary WASM format).\n\n```\n# Compile to human-readable WAT\nuv run python -m tooling.wasm.main script.tcl --format wat\n\n# Compile to WASM binary with optimisations\nuv run python -m tooling.wasm.main script.tcl -O --format wasm -o out.wasm\n\n# Compare optimised vs. unoptimised output\nuv run python -m tooling.wasm.main --source 'set x [expr {1+2}]' --format both\n```\n\nA standalone web UI for the compiler explorer, available in two variants: offline (bundles Pyodide) and CDN (loads Pyodide from jsDelivr).\n\n```\n# Standalone (offline, ~100 MB)\n./tcl-lsp-explorer-gui.pyz --port 8080\n\n# CDN variant (lightweight, requires internet)\n./tcl-lsp-explorer-gui-cdn.pyz --port 8080\n```\n\nA bytecode interpreter that compiles and executes Tcl scripts using the compiler pipeline, with an interactive REPL and disassembly mode. Supports TclOO classes (constructors, destructors, methods, mixins, filters, private variables), namespaces, coroutine-free control flow, and 85% conformance against Tcl 9.0.3 native test suites.\n\n```\n# Execute a script\nuv run python -m tooling.vm script.tcl arg1 arg2\n\n# Interactive REPL\nuv run python -m tooling.vm\n\n# Inline evaluation\nuv run python -m tooling.vm -e 'puts [expr {6 * 7}]'\n\n# Show bytecode disassembly without executing\nuv run python -m tooling.vm --disassemble script.tcl\n```\n\nAn interactive debugger that can single-step through Tcl scripts with breakpoints, variable inspection, and call stack visualisation. Three backends are available:\n\n| Backend | Description |\n|---|---|\n`vm` |\nThe project's own bytecode VM (default) |\n`tclsh` |\nExternal `tclsh` subprocess |\n`tkinter` |\nPython's built-in `tkinter.Tcl()` interpreter |\n\n```\n# Debug a script (uses VM backend by default)\nuv run python -m debugger script.tcl\n\n# Force a specific backend\nuv run python -m debugger --backend vm script.tcl\n\n# Read from stdin\necho 'puts hello' | uv run python -m debugger -\n```\n\nDebugger commands: `run`\n\n, `step`\n\n/`s`\n\n, `next`\n\n/`n`\n\n, `finish`\n\n, `continue`\n\n/`c`\n\n,\n`break <line>`\n\n/`b`\n\n, `delete <id>`\n\n/`d`\n\n, `vars`\n\n, `print <var>`\n\n/`p`\n\n, `stack`\n\n,\n`list`\n\n/`l`\n\n, `quit`\n\n/`q`\n\n.\n\nThe server ships a registry of command signatures, argument roles, and validation rules keyed by dialect. Switching the dialect profile changes which commands are known, which are deprecated, and which event/layer constraints apply.\n\nThe dialect is selected automatically using the following priority chain (highest to lowest):\n\n-\n**Editor language ID**-- opening a file as`tcl-irule`\n\n,`tcl8.4`\n\n, etc. selects the matching dialect immediately. -\n**File extension**--`.irul`\n\n/`.irule`\n\n→`f5-irules`\n\n,`.iapp`\n\n/`.iappimpl`\n\n/`.impl`\n\n→`f5-iapps`\n\n,`.exp`\n\n→`expect`\n\n. -\n**Comment directive**-- a`# tcl-dialect: <dialect>`\n\ncomment in the first 5 lines of a file pins the dialect for that file:\n\n```\n# tcl-dialect: tcl8.4\nset x 1\n```\n\n-\n**Shebang**--`#!/usr/bin/env tclsh8.5`\n\nselects`tcl8.5`\n\n;`#!/usr/bin/expect`\n\nselects`expect`\n\n. -\n**User setting**-- the`tclLsp.dialect`\n\nconfiguration value acts as the default for files that have no per-file hint. -\n**Hardcoded fallback**--`tcl8.6`\n\nwhen nothing else matches.\n\nPer-file hints (directive, shebang, extension) always take priority over the global setting, so different files in the same workspace can target different Tcl versions without manual switching.\n\n| Dialect | Description |\n|---|---|\n`tcl8.4` |\nTcl 8.4 core commands |\n`tcl8.5` |\nTcl 8.5 core commands (adds `{*}` , `lassign` , `dict` , etc.) |\n`tcl8.6` |\nTcl 8.6 core commands (adds `try` /`finally` , `tailcall` , coroutines) -- default |\n`tcl9.0` |\nTcl 9.0 core commands (adds `lpop` , zipfs, updated `encoding` ) |\n`tcl9.1` |\nTcl 9.1 core commands (superset of 9.0; adds the `unicode` and `timer` ensembles and `subst` 's positive `-backslashes` /`-commands` /`-variables` options) |\n`f5-irules` |\nF5 BIG-IP iRules: HTTP/SSL/DNS/LB namespaces, event-validity checks, taint analysis, `static::` scoping rules |\n`f5-iapps` |\nF5 iApps template commands |\n`f5-bigip` |\nF5 BIG-IP configuration (`bigip.conf` ) commands |\n`synopsys-eda-tcl` |\nSynopsys EDA commands (Design Compiler, PrimeTime, ICC2, Formality) |\n`cadence-eda-tcl` |\nCadence EDA commands (Genus, Innovus, Tempus, Xcelium) |\n`xilinx-eda-tcl` |\nXilinx/AMD EDA commands (Vivado, Vitis) |\n`intel-quartus-eda-tcl` |\nIntel Quartus Prime commands |\n`mentor-eda-tcl` |\nMentor/Siemens EDA commands (ModelSim, Questa, Calibre) |\n`expect` |\nExpect: `spawn` , `expect` , `send` , `interact` and related commands for automating interactive programs |\n\n**Tk**, **tcllib**, and **Tcl stdlib** commands are automatically recognised\nwhen the corresponding `package require`\n\nappears in the file. No manual\ntoggle is needed — the registry activates the relevant command definitions\nper-document.\n\nFor commands that the LSP does not know about (custom extensions, vendor tools, internal frameworks), you can declare stubs so the LSP understands their signatures. Two mechanisms are supported:\n\n**External stub files** (`<name>.tcl.stubs`\n\n):\n\n``` js\n# synopsys.tcl.stubs\nstub foreach_in_collection {varName:var collection body:body} -loop\nstub get_cells {?-hierarchical? ?-filter? pattern:pattern} -pure\nstub sizeof_collection {collection} -pure\nstub expr-func sizeof 1\n```\n\n**Inline stubs** (in any `.tcl`\n\nfile, using markers):\n\n``` js\n# tcl-lsp: stubs-begin\n# tcl-lsp: stub foreach_in_collection {varName:var collection body:body} -loop\n# tcl-lsp: stub get_cells {pattern:pattern} -pure\n# tcl-lsp: stub expr-func sizeof 1\n# tcl-lsp: stub expr-op contains 2\n# tcl-lsp: stubs-end\n```\n\nMultiple stubs blocks per file are supported. Argument roles include\n`body`\n\n, `expr`\n\n, `var`\n\n, `var_read`\n\n, `name`\n\n, `pattern`\n\n, `channel`\n\n, and\n`value`\n\n(default). Flags include `-barrier`\n\n, `-loop`\n\n, `-pure`\n\n,\n`-mutator`\n\n, `-unsafe`\n\n, and `-scope_alias`\n\n.\n\nExpression stubs declare custom math functions (`expr-func`\n\n) and infix\noperators (`expr-op`\n\n) with optional arity.\n\nSee [KCS: Dialect stubs](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-dialect-stubs.md) for full syntax.\n\nWhen `interp alias {} name {} target ?args?`\n\ncreates a command alias in the\ncurrent interpreter, the LSP automatically inherits the target command's\nargument semantics. This means expression arguments, body arguments, variable\nnames, and patterns are all correctly analysed through the alias:\n\n```\ninterp alias {} = {} expr\nproc calculate {x y} {\n    set result [= {$x + $y}]   ;# $x and $y recognised as reads — no W214\n    return $result\n}\n```\n\nAlias information is also used by LSP features: **hover** shows the target\ncommand's documentation, **completion** offers aliases as candidates,\n**go-to-definition** follows aliases to the target proc, and\n**signature help** shows the target's parameter hints.\n\nSee [KCS: Command alias resolution](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-command-alias-resolution.md)\nfor details.\n\nThe analyser automatically infers how each proc parameter is used inside the proc body, producing structured trait annotations:\n\n| Trait | Detected pattern |\n|---|---|\n`EVAL` |\n`eval $param` , `uplevel 1 $param` |\n`BODY` |\n`foreach item $list $param` |\n`VAR_WRITE` |\n`upvar 1 $param local; set local 42` |\n`VAR_READ` |\n`upvar 1 $param local; return $local` |\n`EXPR` |\n`if {$param} {...}` |\n`LOOP_LIST` |\n`foreach item $param {...}` |\n\nTwo analysis tiers: a fast shallow pass (synchronous, top-level commands) and a deep pass (asynchronous, recursive descent into nested bodies). Traits feed optimisation, shimmer analysis, taint propagation, and diagnostics.\n\nSee [KCS: Proc arg traits](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-proc-arg-traits.md) for details.\n\n`Tcl: Insert Tcl Template Snippet`\n\n-- quick-pick and insert any bundled Tcl/iRules snippet template.`Tcl: Insert iRule Event Skeleton`\n\n-- scaffold selected iRules events into a new Tcl buffer.`Tcl: Scaffold Tcl Package Starter`\n\n-- generate package layout, tests, CI workflow, and README.`Tcl: Insert package require`\n\n-- suggest and insert`package require`\n\nlines based on symbol usage.`Tcl: Apply Safe Quick Fixes`\n\n-- apply all non-overlapping safe quick fixes in one pass.`Tcl: Run Runtime Validation`\n\n-- run dialect-aware runtime checks on demand.\n\nThe formatter supports full-document and range formatting via the standard LSP\n`textDocument/formatting`\n\nand `textDocument/rangeFormatting`\n\nrequests. Defaults\nfollow the [F5 iRules Style Guide](https://community.f5.com/kb/technicalarticles/irules-style-guide/305921).\n\nCapabilities include:\n\n**Indentation**-- configurable size, spaces or tabs, with separate continuation indent** Brace placement**-- K&R (end of line) style** Expression bracing**-- optionally enforce`expr {$x + 1}`\n\ninstead of`expr $x + 1`\n\n**Variable bracing**-- optionally rewrite`$var`\n\nas`${var}`\n\n**Line length**-- hard limit and soft goal; long lines are wrapped at continuation points** Semicolons**-- convert`;`\n\n-separated commands to individual lines**Body expansion**-- optionally expand single-line`if`\n\n/`foreach`\n\n/etc. bodies to multi-line**Blank lines**-- normalise spacing between procs, between control-flow blocks, and cap consecutive blank lines** Comments**-- ensure space after`#`\n\n, align inline comments to a consistent column**Whitespace**-- trim trailing whitespace, ensure final newline, normalise line endings (LF/CRLF/CR)** Docstrings**-- configurable style (preceding or body-internal), doxygen or plain tag format, optional decoration borders\n\nThe formatter also recognises multi-line docstrings with `@param`\n\n, `@return`\n\n,\nand `@brief`\n\ntags (doxygen-style) and displays them as structured hover\ninformation. Body-internal docstrings (comment blocks at the start of a proc\nbody) are supported as a fallback when no preceding comment exists.\n\nAll options are exposed through `tclLsp.formatting.*`\n\nsettings (see\n[Configuration](#formatter-settings) below).\n\nDiagnostics can be suppressed at five different scopes. Smaller scope is always better — turning a code off globally hides real problems in future projects.\n\n| Scope | How |\n|---|---|\n| One command | `# noqa: CODE` on the line before the command |\n| One file | `# tcl-lsp: disable=CODE,CODE` near the top of the file |\n| One project | `[diagnostics]\\ndisabled = CODE` in `.tcl-lsp.ini` at the workspace root |\n| One editor | `tclLsp.diagnostics.CODE: false` in editor settings |\n| Everywhere | `[diagnostics]\\ndisabled = CODE` in the\n|\n\n**Inline** — put on the line *before* the command:\n\n```\n# noqa: W100\nexpr $x + 1\n\n# noqa: *\neval $user_input\n```\n\n**Top-of-file** — before the first non-comment line:\n\n``` bash\n#!/usr/bin/env tclsh\n# tcl-lsp: disable=W100,O111\n```\n\n**Project config** — `.tcl-lsp.ini`\n\nat the workspace root (commit with source):\n\n```\n[diagnostics]\ndisabled = W111, IRULE1005\n\n[optimiser]\ndisabled = O109\n```\n\nFor the complete reference, see\n[ docs/kcs/kcs-howto-suppress-diagnostics.md](/bitwisecook/tcl-lsp/blob/main/docs/kcs/kcs-howto-suppress-diagnostics.md).\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| E001 | Missing required subcommand | |\n| E002 | Too few arguments | |\n| E003 | Too many arguments | |\n| E100 | Unmatched `]` -- missing opening `[` |\nInsert `[` |\n| E101 | Missing `{` after `switch` -- body cases follow without braces |\n|\n| E102 | Unmatched `}` -- missing opening `{` |\nRemove stray `}` |\n| E103 | Missing `}` -- a nested body consumed this closing brace |\n|\n| E200 | Parse error -- internal representation cannot be determined |\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| W001 | Unknown subcommand | |\n| W002 | Command is disabled in active dialect profile | |\n| W003 | Expression operator not available in the active dialect | |\n| W004 | Command option not available in the active dialect | |\n| W100 | Unbraced `expr` /`if` /`while` /`for` expression (double substitution risk) |\nWrap in braces |\n| W104 | `append` with space-separated values (use `lappend` for lists) |\n|\n| W105 | Unbraced code block or missing `variable` declaration in `namespace eval` |\nWrap in braces |\n| W106 | Dangerous unbraced `switch` body |\n|\n| W108 | Non-ASCII characters in token content (smart quotes, non-breaking spaces) | Replace with ASCII |\n| W110 | `==` /`!=` on strings in `expr` (use `eq` /`ne` ) |\nReplace operator |\n| W111 | Line exceeds configured maximum length | |\n| W112 | Trailing whitespace | Remove whitespace |\n| W113 | Procedure shadows a built-in command | |\n| W114 | Redundant nested `[expr]` -- already in expression context |\n|\n| W115 | Backslash-newline in comment silently swallows the next line | Convert to per-line comments |\n| W116 | Stub command shadows a built-in command | |\n| W117 | Stub expression definition shadows a built-in function or operator | |\n| W118 | Inconsistent line endings | |\n| W120 | Package-gated command used without `package require` |\nInsert `package require` |\n| W121 | Subnet mask has non-contiguous bits | Replace with nearest valid mask |\n| W122 | Mistyped IPv4 address (octet > 255 or leading zero) | |\n| W123 | Unknown command — not found in registry, user procs, or `unknown` handler (opt-in) |\nReplace with suggestion |\n| W124 | Invalid IP address literal | |\n| W125 | Orphaned control-flow keyword used as a standalone command | |\n| W126 | Non-channel value in channel argument position | |\n| W127 | Value not in the command's allowed set (e.g. `HTTP::version \"2.0\"` ) |\nUse one of the listed values |\n| W200 | Binary format modifier requires newer Tcl | |\n| W201 | Manual path concatenation — uses rendered value properties and taint suppression (use `file join` ) |\nRewrite as `[file join]` |\n| W230 | Constant list index out of range -- `lindex` /`lrange` /`lreplace` silently return empty or clamp |\n|\n| W231 | Constant list index out of range -- `lset` raises a runtime error |\n|\n| W232 | Constant string index out of range -- `string index` /`range` /`replace` /`insert` silently no-op |\n|\n| W240 | Loop condition is constant false -- body never executes | |\n| W241 | Loop is provably infinite -- constant-true condition with no `break` /`return` |\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| H300 | Possible paste error -- repeated assignment to same variable with same value | |\n| W210 | Variable read before set (with case-mismatch suggestion when applicable; `info exists` /`array exists` are existence tests, not reads, so they are excluded and instead fold to a constant branch where provable) |\n|\n| W211 | Variable set but never used (with case-mismatch suggestion when applicable) | |\n| W212 | Variable substitution where name expected (`set $x` , `incr $x` , `info exists $x` , etc.) |\n|\n| W213 | `unset` on variable that may not exist -- use `unset -nocomplain` |\n|\n| W214 | Unused proc parameter -- argument declared but never read in the body | |\n| W215 | Variable name unreachable via `$` -substitution (creatable, but no `$` -form can read it) |\n|\n| W216 | Broken brace-form array element reference (`${arr}(x)` parses as scalar + literal) |\n|\n| W220 | Dead store -- variable set but overwritten before use (with case-mismatch suggestion when applicable) |\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| W101 | `eval` with substituted arguments (code injection risk) |\n|\n| W102 | `subst` with a variable argument (template injection risk) |\n|\n| W103 | `open` with pipeline or variable argument (command injection risk) |\n|\n| W300 | `source` with a variable path (code execution risk) |\n|\n| W301 | `uplevel` with unbraced or multi-arg script (injection risk) |\n|\n| W303 | `regexp` with nested quantifiers (ReDoS risk) |\n|\n| W304 | Missing `--` on option-bearing commands before positional input |\nInsert `--` |\n| W306 | Substitution in literal-expected argument position | |\n| W307 | Non-literal command name (variable or command substitution as command) | |\n| W308 | `subst` without `-nocommands` |\n|\n| W309 | `eval` /`uplevel` with `subst` -- double substitution risk |\n|\n| W310 | Hardcoded credentials (API keys, tokens, passwords) | |\n| W311 | Unsafe channel encoding mismatch (`-encoding binary` with `-translation` ) |\n|\n| W312 | `interp eval` /`interp invokehidden` with dynamic script (injection risk) |\n|\n| W313 | Destructive `file` operations (`delete` /`rename` /`mkdir` ) with variable path |\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| W130 | `tclpkg.tcl` requires a package not in `tclpkg.lock` |\nRun `tcl pkg install` |\n| W131 | `tclpkg.lock` is out of sync with `tclpkg.tcl` |\nRun `tcl pkg install` |\n| W132 | `tclpkg.lock` integrity mismatch -- CAS hash differs from lockfile |\n|\n| W133 | `tclpkg.tcl` directive not permitted in safe mode |\n|\n| W134 | Package resolved but no `pkgIndex.tcl` found -- `package require` will fail at runtime |\n\n| Code | Description | Quick-fix |\n|---|---|---|\n| W242 | Loop termination cannot be proven -- counter not provably modified by the body or step | |\n| W302 | `catch` without a result variable (silently swallows errors) |\nAdd result variable |\n\nThe shimmer analyser tracks each variable's Tcl internal representation (\"intrep\") through the SSA type lattice. When a command expects a different intrep than the variable currently holds, Tcl must destroy and recreate the representation -- a \"shimmer\". This is normally invisible but can be a significant performance cost in loops.\n\n| Code | Severity | Description |\n|---|---|---|\n| S100 | Info | Single shimmer outside a loop |\n| S101 | Warning | Shimmer inside a loop body (per-iteration cost) |\n| S102 | Warning | Variable oscillates between two types across loop iterations (type thunking) |\n\nThe taint analyser tracks data provenance through the SSA graph using a\ncolour-aware lattice. Values originating from I/O commands (network reads,\nfile reads, process execution) are tagged as tainted. Taint propagates\nthrough assignments, string interpolation, and phi nodes. Commands that\nproduce fixed-type results (e.g. `string length`\n\n, `llength`\n\n) act as\nsanitisers.\n\nTaint colours carry value properties (e.g. `PATH_NORMALISED`\n\nfor values\nnormalised via `file normalize`\n\n, `PATH_JOINED`\n\nfor values assembled via\n`file join`\n\n). At join points, colours are intersected so only properties\nshared by all paths survive -- this suppresses false positives.\n\nThe **Rendered Value Properties** pass (`compiler/rendered_properties.py`\n\n)\nruns before taint propagation and computes per-SSA-value string content\nproperties after Tcl backslash substitution. This enables precise detection\nof path separators (resolving escape sequences like `\\x2f`\n\nto `/`\n\nbefore\nchecking) and is used by the W201 path concatenation diagnostic.\n\n| Code | Severity | Description | Quick-fix |\n|---|---|---|---|\n| T100 | Warning | Tainted data flows into a dangerous code-execution sink | |\n| T101 | Warning | Tainted data flows into an output command | |\n| T102 | Warning | Tainted data in option position without `--` terminator |\nInsert `--` |\n| T103 | Warning | Tainted data in `regexp` /`regsub` pattern (regex injection / ReDoS risk) |\nWrap with `[regex::quote]` |\n| T104 | Warning | Tainted data in network address argument (SSRF risk) | |\n| T105 | Warning | Tainted data in `interp eval` script argument (cross-interpreter injection) |\n|\n| T106 | Info | Double-encoding -- value already carries encoding colour | Remove redundant encoder |\n\nThese diagnostics fire only in the `f5-irules`\n\ndialect.\n\n| Code | Severity | Description | Quick-fix |\n|---|---|---|---|\n| IRULE1001 | Warning/Hint | Command invalid or ineffective in this iRules event | |\n| IRULE1002 | Warning | Unknown iRules event name | |\n| IRULE1003 | Warning | Deprecated iRules event | |\n| IRULE1004 | Hint | `when` block missing explicit `priority` |\n|\n| IRULE1005 | Warning | `*_DATA` event handler without matching `*::collect` call |\nBootstrap `collect` |\n| IRULE1006 | Warning | `*::payload` access without matching `*::collect` call |\nBootstrap `collect` |\n| IRULE1007 | Error | `*::collect` without matching `*::release` on the same connection side |\n|\n| IRULE1008 | Error | `*::release` without matching `*::collect` on the same connection side |\n|\n| IRULE1201 | Warning | HTTP command used after `HTTP::respond` /`HTTP::redirect` |\n|\n| IRULE1202 | Warning | Multiple `HTTP::respond` /`HTTP::redirect` on different branches |\n\n| Code | Severity | Description | Quick-fix |\n|---|---|---|---|\n| IRULE2001 | Warning | Deprecated `matchclass` -- use `class match` |\nAuto-replace |\n| IRULE2002 | Warning | Deprecated iRules command | |\n| IRULE2003 | Error | Unsafe iRules command (context escalation risk) |\n\n| Code | Severity | Description | Quick-fix |\n|---|---|---|---|\n| IRULE3001 | Warning | Tainted data in HTTP response body (XSS risk) | Wrap with `[HTML::encode]` |\n| IRULE3002 | Warning | Tainted data in HTTP header or cookie value (header injection) | Wrap with `[URI::encode]` |\n| IRULE3003 | Warning | Tainted data in `log` command (log injection) |\n|\n| IRULE3004 | Warning | Tainted data in `HTTP::redirect` URL (open redirect risk) |\n|\n| IRULE3101 | Warning | `HTTP::uri` /`HTTP::path` set to value not provably starting with `/` |\n|\n| IRULE3102 | Warning | `HTTP::path` /`HTTP::uri` /`HTTP::query` getter used without `-normalized` |\n|\n| IRULE3103 | Info | `*::uri` used where `*::path` or `*::query` suffices (`split` , `starts_with` , `contains` , `string match` , etc.) |\n\n| Code | Severity | Description |\n|---|---|---|\n| IRULE4001 | Warning | Write to `static::` variable outside `RULE_INIT` (race condition) |\n| IRULE4002 | Hint | Generic `static::` variable name — collision likely across iRules |\n| IRULE4003 | Hint | Variable scoping concern across events |\n| IRULE4004 | Info | Constant `set` in per-request event could be hoisted to per-connection |\n| IRULE4005 | Warning | Potential race — `static::` variable written outside `RULE_INIT` and read in another event |\n\n| Code | Severity | Description | Quick-fix |\n|---|---|---|---|\n| IRULE2101 | Hint | Heavy `regexp` in a high-frequency event |\n|\n| IRULE5001 | Hint | Ungated `log` in a high-frequency event |\n|\n| IRULE5002 | Warning | `drop` /`reject` /`discard` without `event disable all` or `return` |\nAdd `event disable all` + `return` |\n| IRULE5003 | Hint | Loop condition `$var != 0` can miss zero if decremented past it |\n|\n| IRULE5004 | Warning | `DNS::return` without `return` |\nAdd `return` |\n| IRULE5005 | Error | Direct proc invocation without `call` in iRules |\nPrefix with `call` |\n| IRULE5006 | Warning | Top-level-only command used inside a nested body | |\n| IRULE5007 | Warning | Event-context command used at top level outside a `when` block |\n\nThe optimiser operates on the SSA/CFG intermediate representation and suggests\nsource-level rewrites. All optimiser diagnostics appear at **Information**\nseverity and include a quick-fix code action with the suggested replacement.\n\nFive named profiles control which passes run. Individual codes can be\noverridden via `tclLsp.optimiser.*`\n\nsettings.\n\n| Code | Category | Description | readability | standard | full |\n|---|---|---|---|---|---|\n| O100 | constant_folding | Propagate constant variables into expressions and command arguments. | ✓ | ✓ | |\n| O101 | constant_folding | Fold constant integer expressions. | ✓ | ✓ | |\n| O102 | constant_folding | Fold constant `[expr {...}]` command substitutions. |\n✓ | ✓ | |\n| O103 | constant_folding | Fold static procedure calls using interprocedural summaries. | ✓ | ✓ | |\n| O104 | pattern | Fold static string build chains into a single assignment. | ✓ | ✓ | |\n| O105 | constant_folding | Propagate constants into variable references and detect redundant computations (GVN/CSE). | ✓ | ✓ | |\n| O106 | code_motion | Hoist loop-invariant computations. | ✓ | ||\n| O107 | dce | Eliminate unreachable dead code. | ✓ | ||\n| O108 | dce | Eliminate transitively dead code. | ✓ | ||\n| O109 | dce | Eliminate dead stores. | ✓ | ||\n| O110 | constant_folding | Canonicalise expressions (InstCombine). | ✓ | ✓ | |\n| O111 | readability | Brace expression performance hints (paired with W100). | ✓ | ✓ | ✓ |\n| O112 | dce | Eliminate constant-condition compound statements. | ✓ | ||\n| O113 | constant_folding | Strength-reduce expressions (`x**2` → `x*x` , `x%8` → `x&7` ). |\n✓ | ✓ | |\n| O114 | readability | Recognise `incr` idiom (`set x [expr {$x + N}]` → `incr x N` ). |\n✓ | ✓ | ✓ |\n| O115 | readability | Remove redundant nested `[expr {...}]` in expression context. |\n✓ | ✓ | ✓ |\n| O116 | constant_folding | Fold constant `[list a b c]` to literal value. |\n✓ | ✓ | |\n| O117 | readability | Simplify `[string length $s] == 0` → `$s eq \"\"` . |\n✓ | ✓ | ✓ |\n| O118 | constant_folding | Fold constant `[lindex {a b c} 1]` to element. |\n✓ | ✓ | |\n| O119 | pattern | Pack consecutive `set` literals into `lassign` /`foreach` . |\n✓ | ✓ | |\n| O120 | readability | Prefer `eq` /`ne` over `==` /`!=` for string comparisons. |\n✓ | ✓ | ✓ |\n| O121 | recursion | Rewrite self-recursive tail calls to `tailcall` . |\n✓ | ||\n| O122 | recursion | Convert fully tail-recursive proc to iterative `while` loop. |\n✓ | ||\n| O123 | recursion | Detect non-tail recursion eligible for accumulator introduction (hint only). | ✓ | ||\n| O124 | dce | Comment out unused procs in iRules (not called from any event). | ✓ | ||\n| O125 | code_motion | Sink side-effect-free assignments into the deepest decision block (`if` /`switch` ) that uses them. |\n✓ | ||\n| O126 | dce | Remove unused variable assignments — eliminate `set` statements for variables that are never read. |\n✓ | ||\n| O127 | code_motion | Inline single-use variable assignment — eliminate redundant variable load by folding `set` into the use site. |\n✓ | ||\n| O128 | readability | Rewrite `[expr {[llength $L] - N}]` / `[expr {[string length $s] - N}]` to `end-(N-1)` when used as an index argument to `lindex` (first index), `lrange` , `lreplace` , `string index` , `string range` , or `string replace` with a matching container reference. |\n✓ | ✓ | ✓ |\n\n**Profiles:** `off`\n\ndisables all passes. `readability`\n\n, `standard`\n\n, and `full`\n\nenable\nprogressively more passes (single-pass). `aggressive`\n\n= `full`\n\nwith multi-pass\nto fixpoint (up to 5 iterations). The default editor profile is `readability`\n\n;\nexplicit actions (CLI, chat, MCP) default to `full`\n\n.\n\n- Python 3.10+\n[uv](https://docs.astral.sh/uv/)(Python package manager)- Node.js 24+ with npm (pinned to v12 via\n`packageManager`\n\n; run`corepack enable npm`\n\n) - VS Code 1.93+\n\n```\n# Clone and enter the repo\ngit clone <repo-url>\ncd tcl-lsp\n\n# Run tests\nmake test\n\n# Build the .vsix\nmake build-editor-vsix\n\n# Install in VS Code\ncode --install-extension tcl-lsp-vscode-0.1.0.vsix\n```\n\nRun `make help`\n\nto see all targets:\n\n| Target | Description |\n|---|---|\n`make ci-fast` |\nFull CI gate — lint + Python tests + extension tests + smoke tests |\n`make build-editor-vsix` |\nBuild the .vsix (tests must pass first) |\n`make install` |\nBuild and install the .vsix into VS Code |\n`make package-vsix` |\nPackage VSIX (skip lint/test, for CI) |\n`make test` |\nRun all tests (Python + VS Code extension) |\n`make test-py` |\nRun the Python test suite only |\n`make test-ext` |\nRun VS Code extension integration tests |\n`make lint` |\nRun all lint and style checks |\n`make lint-py` |\nLint Python code with Ruff |\n`make typecheck-py` |\nType-check Python code with ty |\n`make lint-ts` |\nLint/format-check TypeScript extension code |\n`make format-py` |\nFormat and auto-fix Python code with Ruff |\n`make npm-env` |\nInstall/update npm dependencies |\n`make compile` |\nCompile the TypeScript extension |\n`make zipapps` |\nBuild all zipapps (Tcl, explorer-cli, explorer-gui, explorer-gui-cdn, LSP, AI, MCP, WASM) |\n`make zipapp-tcl` |\nBuild the unified Tcl tools zipapp |\n`make zipapp-explorer-cli` |\nBuild the compiler-explorer CLI zipapp |\n`make zipapp-explorer-gui` |\nBuild the standalone explorer GUI zipapp (bundles Pyodide) |\n`make zipapp-explorer-gui-cdn` |\nBuild the CDN explorer GUI zipapp (loads Pyodide from CDN) |\n`make zipapp-lsp` |\nBuild the LSP server zipapp |\n`make zipapp-ai` |\nBuild the AI analysis zipapp |\n`make zipapp-mcp` |\nBuild the MCP server zipapp |\n`make zipapp-wasm` |\nBuild the WASM compiler zipapp |\n`make claude-skills` |\nBuild Claude Code skills release zip |\n`make build-editor-jetbrains` |\nBuild the JetBrains plugin (.zip) |\n`make build-editor-sublime` |\nBuild the Sublime Text package (.sublime-package) |\n`make build-editor-zed` |\nBuild the Zed extension (.tar.gz WASM artifact) |\n`make screenshot` |\nAlias of `make screenshots` |\n`make screenshots` |\nCapture extension screenshots and build demo GIF (macOS) |\n`make release` |\nBuild all release artifacts (parity with tagged CI release jobs) |\n`make release-tag` |\nBump version, annotated-tag, and push (`V=x.y.z` ) |\n`make clean` |\nRemove build artifacts |\n`make distclean` |\nRemove build artifacts and `node_modules` |\n\nArtifact version strings are derived from `git describe`\n\n(with `v`\n\nstripped).\nIf Git metadata is unavailable, builds fall back to `dev`\n\n(and semver-constrained\nmanifest fields use `0.0.0-dev`\n\n).\n\n`make build-editor-vsix`\n\nis the main entry point. It runs the test suite first and will\nnot package a .vsix if any test fails. Packaging uses an isolated staging\ndirectory under `build/vsix-stage/`\n\n, and the output file lands under\n`build/`\n\nas `tcl-lsp-<version>.vsix`\n\n.\n\nOn macOS, `make screenshots`\n\nprefers a small Swift window-probe helper when\n`swiftc`\n\nis available, so captures use deterministic\n`screencapture -o -l <window-id>`\n\n. If Swift is unavailable, it falls back to\nAppleScript-based probing.\nBy default, `make screenshots`\n\nauto-installs missing screenshot tools with\nHomebrew (`pngquant`\n\n, `oxipng`\n\n, `gifsicle`\n\n, and `imagemagick`\n\nwhen needed).\nTo disable auto-install, run:\n`TCL_LSP_SCREENSHOT_AUTO_BREW=0 make screenshots`\n\n.\nBy default, screenshot runs are isolated:\n\n- downloaded VS Code\n`stable`\n\nvia`@vscode/test-electron`\n\n- isolated user data (\n`~/.tcl-lsp-screenshots/user-data`\n\n) - isolated extensions dir (\n`~/.tcl-lsp-screenshots/extensions`\n\n) - allowlisted external extensions only (\n`github.copilot-chat`\n\n)\n\nUseful overrides:\n\n- Reuse your normal VS Code user data:\n`TCL_LSP_SCREENSHOT_REUSE_CODE_USER_DATA=1 make screenshots`\n\n- Use local app bundle instead of downloaded VS Code:\n`TCL_LSP_SCREENSHOT_USE_SYSTEM_VSCODE=1 TCL_LSP_SCREENSHOT_FORCE_DOWNLOADED_VSCODE=0 make screenshots`\n\n- Change allowed external extensions (comma-separated extension IDs):\n`TCL_LSP_SCREENSHOT_ALLOWED_EXTENSIONS=github.copilot-chat make screenshots`\n\n- Production dependency audits are enforced with\n`npm audit --omit=dev`\n\n. - Dev-only audit findings are accepted and do not block releases in this repository.\n\n```\ntcl-lsp/\n  Makefile                Build system\n  pyproject.toml          Python project metadata (hatchling)\n  server/                    Python LSP server\n    __main__.py           Entry point (python -m server)\n    server.py             pygls server, handler wiring\n    async_diagnostics.py  Background diagnostic scheduler (tiered publishing)\n    analysis/\n      analyser.py         Single-pass semantic analyser\n      checks.py           Best-practice and security checks (W-series)\n      irules_checks.py    iRules-specific best-practice checks (IRULE-series)\n      semantic_model.py   Data model (scopes, procs, diagnostics)\n      semantic_graph.py   Call/symbol/data-flow graph queries\n    bigip/\n      parser.py           BIG-IP configuration file parser\n      model.py            BIG-IP configuration data model\n      rule_extract.py     iRule extraction from BIG-IP configs\n      validator.py        Configuration validation\n      diagnostics.py      BIG-IP-specific diagnostics\n    commands/\n      registry/\n        models.py         CommandSpec dataclass (arity, roles, dialect flags)\n        command_registry.py CommandRegistry class (query methods)\n        runtime.py        Registry runtime (dialects, roles, body/expr index helpers)\n        signatures.py     Argument signature helpers\n        namespace_registry.py Namespace registry (event/command metadata facade)\n        namespace_data.py    Canonical event/command data tables\n        namespace_models.py  Namespace model dataclasses\n        operators.py      Operator definitions and hover data\n        taint_hints.py    Per-command taint source/sink hints\n        type_hints.py     Per-command return type hints\n        tcl/              One file per Tcl command (@register decorator)\n        irules/           F5 iRules command definitions\n        iapps/            F5 iApps template command definitions\n        tk/               Tk widget command definitions\n        tcllib/           tcllib package command definitions\n        stdlib/           Tcl standard library command definitions\n    common/\n      dialect.py          Active dialect state\n      naming.py           Name normalisation helpers\n      ranges.py           Range/position utilities\n    packages/\n      resolver.py         Tcl package require resolution\n    compiler/\n      lowering.py         Tcl source -> IR lowering\n      ir.py               IR node definitions\n      cfg.py              Control flow graph construction\n      ssa.py              Static single assignment form\n      core_analyses.py    SCCP, liveness, type inference, dead store detection\n      compilation_unit.py Compile pipeline orchestration and caching\n      compiler_checks.py  IR-to-diagnostics (arity, subcommands)\n      optimiser.py        Source rewrite passes (O100–O128)\n      gvn.py              GVN/CSE/PRE/LICM redundant computation detection (O105–O106)\n      interprocedural.py  Call graph, function purity/side-effect summaries\n      taint.py            Data taint analysis (T100–T106, IRULE3xxx)\n      shimmer.py          Tcl object representation analysis (S100–S102)\n      irules_flow.py      iRules control-flow checks (IRULE1xxx/4004/5xxx)\n      codegen.py          Tcl VM bytecode assembly backend\n      static_loops.py     Conservative static evaluation for for-loops\n      tcl_expr_eval.py    Tcl expression evaluator (constant folding)\n      expr_ast.py         Expression AST parser\n      expr_types.py       Expression type inference\n      effects.py          Command side-effect classification\n      connection_scope.py iRules connection-scope variable tracking\n      types.py            Type lattice definitions\n      token_helpers.py    Shared token-stream utilities\n      eval_helpers.py     Evaluation helper constants\n    diagram/\n      extract.py          iRule event-flow diagram extraction\n    features/\n      code_actions.py     Quick-fix code actions\n      completion.py       Completions\n      definition.py       Go to definition\n      diagnostics.py      Diagnostic aggregation (internal -> LSP)\n      formatting.py       LSP formatting handlers\n      hover.py            Hover information\n      inlay_hints.py      Inlay hint provider (inferred types, format strings)\n      references.py       Find references\n      rename.py           Rename symbol\n      call_hierarchy.py   Call hierarchy (incoming/outgoing calls)\n      document_symbols.py Document symbol hierarchy\n      document_links.py   Document link provider\n      folding.py          Folding range provider\n      selection_range.py  Selection range provider\n      signature_help.py   Signature help provider\n      workspace_symbols.py Workspace symbol search\n      semantic_tokens.py  Semantic token provider\n      snippet_templates.py Tcl/iRules snippet templates\n      symbol_resolution.py Shared word/variable/scope resolution helpers\n    parsing/\n      lexer.py            Tcl lexer with position tracking\n      tokens.py           Token and position types\n      command_segmenter.py Command segmentation from token stream\n      token_scanning.py   Shared token-stream scanning helpers\n      recovery.py         Centralised error recovery via virtual tokens\n      expr_lexer.py       Expression sub-lexer\n      expr_parser.py      Expression sub-parser\n      subst_nocommands.py Compile-time `[subst -nocommands]` evaluator\n    tk/\n      detection.py        Tk widget auto-detection\n      diagnostics.py      Tk-specific diagnostics\n      extract.py          Tk widget hierarchy extraction\n    workspace/\n      document_state.py   Per-file analysis cache (dialect-gated profile scanning)\n      workspace_index.py  Cross-file proc index (O(1) tail lookup, usage caching)\n      scanner.py          Background workspace file scanner\n    xc/\n      translator.py       iRules-to-XC migration translator\n      mapping.py          iRules → XC command mapping table\n      xc_model.py         XC output data model\n      terraform.py        Terraform HCL generation\n      json_api.py         JSON API for XC translation\n      diagnostics.py      Migration diagnostics\n  tooling/explorer/               Compiler explorer (CLI + web GUI)\n    cli.py                CLI interface\n    pipeline.py           Compilation pipeline wrapper\n    serialise.py          Output serialisation (IR, CFG, SSA, optimiser)\n    formatters.py         Display formatters\n    static/               Web GUI assets (Pyodide)\n  ai/                     AI integrations\n    claude/\n      skills/             Claude Code skills (20 CLI commands)\n    mcp/\n      tcl_mcp_server.py   MCP server for Claude Desktop integration\n    prompts/              System prompts for Tcl/iRules/Tk\n    shared/               Shared diagnostics manifest and utilities\n  tests/                  pytest test suite\n  editors/\n    vscode/               VS Code extension client (.vsix)\n      package.json        Extension manifest\n      tsconfig.json       TypeScript config\n      src/extension.ts    Extension entry point\n      language-configuration.json\n      syntaxes/tcl.tmLanguage.json\n    neovim/               Neovim LSP config (Lua) <!-- editors:Neovim -->\n    zed/                  Zed extension (TOML + Rust WASM) <!-- editors:Zed -->\n    emacs/                Emacs eglot / lsp-mode config <!-- editors:Emacs -->\n    helix/                Helix languages.toml config <!-- editors:Helix -->\n    sublime-text/         Sublime Text package (syntax, LSP, snippets) <!-- editors:Sublime Text -->\n    jetbrains/            JetBrains plugin (Gradle/Kotlin) <!-- editors:JetBrains -->\n```\n\nSee `CONTRIBUTING.md`\n\nfor coding-style and packaging rules.\n\nThe server communicates over stdio. To launch it directly:\n\n```\nuv run python -m server\n```\n\nThis is useful for debugging or for use with any LSP client.\nSee `editors/`\n\nfor per-editor setup instructions.\n\n```\n# Via make (sets up the venv automatically)\nmake test\n\n# Or directly with uv\nuv run --extra dev pytest tests/ -v\n\n# Run a specific test file\nuv run --extra dev pytest tests/test_checks.py -v\n\n# Run tests matching a pattern\nuv run --extra dev pytest tests/ -k \"unbraced_expr\"\n\n# Lint Python code\nmake lint-py\n\n# Type-check Python code\nmake typecheck-py\n\n# Auto-fix and format Python code\nmake format-py\n```\n\nUse `tcl_compiler_explorer.py`\n\nto inspect how source is lowered and optimised:\n\n```\n# Full compiler + optimiser exploration\nuv run python tcl_compiler_explorer.py samples/for_screenshots/22-optimiser-before.tcl\n\n# Focus on optimiser rewrites only\nuv run python tcl_compiler_explorer.py samples/for_screenshots/22-optimiser-before.tcl --focus optimiser\n\n# Inline source with explicit optimised output\nuv run python tcl_compiler_explorer.py --source 'set a 1; set b [expr {$a + 2}]' --show-optimised-source\n```\n\nThe explorer renders:\n\n- lowered IR and per-procedure bodies\n- CFG pre-SSA and post-SSA (with use/def and inferred constants)\n- interprocedural summaries\n- optimiser rewrites\n- source callouts with caret markers and\n`+-->`\n\narrows for salient spans\n\n```\n# Install npm deps\nmake npm-env\n\n# Watch mode (recompiles on save)\ncd editors/vscode && npm run watch\n```\n\nTo test the extension in VS Code, open `editors/vscode/`\n\nin VS Code and press\n**F5** to launch the Extension Development Host.\n\nDuring development you can point the extension at your working copy instead\nof the bundled server. Set `tclLsp.serverPath`\n\nin your VS Code settings:\n\n```\n{\n  \"tclLsp.serverPath\": \"/path/to/tcl-lsp\"\n}\n```\n\nThe extension will use `uv run`\n\nfrom that directory, so changes to the Python\nsource take effect on the next editor reload.\n\n- Add a check function to the appropriate submodule in\n`analyser/checks/`\n\n(e.g.`_security.py`\n\n,`_style.py`\n\n,`_domain.py`\n\n,`_syntax.py`\n\n) following the existing pattern -- each check receives the command name, argument texts, argument tokens, all tokens, and the source string. - Register it in the\n`ALL_CHECKS`\n\nlist in`analyser/checks/_orchestrator.py`\n\n. - If the check can be auto-fixed, include a\n`CodeFix`\n\nin the diagnostic's`fixes`\n\ntuple. - Add tests to\n`tests/test_checks.py`\n\n. - Run\n`make test`\n\nto verify.\n\n- Add the field to\n`FormatterConfig`\n\nin`tooling/formatter/config.py`\n\n. - Handle it in\n`tooling/formatter/engine.py`\n\n. - Add\n`to_dict`\n\n/`from_dict`\n\nsupport if the field uses a non-primitive type. - Add tests to\n`tests/test_formatter.py`\n\n. - Import the formatter through its public API (\n`tooling.formatter`\n\n), then run`make test`\n\nto verify.\n\nServer/runtime settings are available through the `tclLsp.*`\n\nnamespace.\n\n| Setting | Default | Description |\n|---|---|---|\n`dialect` |\n`tcl8.6` |\nDefault dialect for files without a shebang or `# tcl-dialect:` comment directive. Per-file hints take priority. |\n`extraCommands` |\n`[]` |\nExtra command names treated as known varargs commands |\n`libraryPaths` |\n`[]` |\nAdditional directories to scan for Tcl packages and libraries |\n\nFormatter options are available through `tclLsp.formatting.*`\n\n(defaults based\non the F5 iRules Style Guide):\n\n| Setting | Default | Description |\n|---|---|---|\n`indentSize` |\n`4` |\nSpaces per indent level |\n`indentStyle` |\n`spaces` |\n`spaces` or `tabs` |\n`continuationIndent` |\n`4` |\nExtra indentation for continuation lines |\n`braceStyle` |\n`k_and_r` |\n`k_and_r` |\n`spaceBetweenBraces` |\n`true` |\nSpace between consecutive braces (`} {` vs `}{` ) |\n`enforceBracedVariables` |\n`false` |\nRewrite `$var` as `${var}` |\n`enforceBracedExpr` |\n`false` |\nRequire braced expressions |\n`maxLineLength` |\n`120` |\nHard line length limit |\n`goalLineLength` |\n`100` |\nSoft target for line length |\n`expandSingleLineBodies` |\n`false` |\nForce multi-line bodies |\n`minBodyCommandsForExpansion` |\n`2` |\nMinimum commands in body before expansion |\n`spaceAfterCommentHash` |\n`true` |\nSpace between `#` and comment text |\n`trimTrailingWhitespace` |\n`true` |\nRemove trailing whitespace |\n`alignCommentsToCode` |\n`true` |\nAlign inline comments to a consistent column |\n`replaceSemicolonsWithNewlines` |\n`true` |\nConvert `;` to newlines |\n`blankLinesBetweenProcs` |\n`1` |\nBlank lines separating proc definitions |\n`blankLinesBetweenBlocks` |\n`1` |\nBlank lines between control flow blocks |\n`maxConsecutiveBlankLines` |\n`2` |\nMaximum consecutive blank lines allowed |\n`lineEnding` |\n`lf` |\nLine ending style (`lf` , `crlf` , `cr` ) |\n`ensureFinalNewline` |\n`true` |\nEnsure file ends with a newline |\n\n| Setting | Default | Description |\n|---|---|---|\n`shimmer.enabled` |\n`true` |\nEnable shimmer detection (S-series diagnostics) |", "url": "https://wpnews.pro/news/an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects", "canonical_source": "https://github.com/bitwisecook/tcl-lsp", "published_at": "2026-08-28 17:28:42+00:00", "updated_at": "2026-08-28 17:48:44.230879+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["bitwisecook", "pygls", "VS Code", "Neovim", "Zed", "Emacs", "Helix", "Sublime Text"], "alternates": {"html": "https://wpnews.pro/news/an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects", "markdown": "https://wpnews.pro/news/an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects.md", "text": "https://wpnews.pro/news/an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects.txt", "jsonld": "https://wpnews.pro/news/an-lsp-for-tcl-8-4-9-1-f5-irules-f5-iapps-and-other-tcl-dialects.jsonld"}}