A language server for Tcl with multi-editor support.
The server is written in Python using pygls and communicates over stdio, making it compatible with any LSP client.
Installation Guide — step-by-step instructions for installing from GitHub Releases on macOS and Windows.
| Editor | Type | Setup | Unique extras |
|---|---|---|---|
| VS Code | Full extension (.vsix) | Install .vsix from Releases |
Compiler explorer panel, Tk preview, @irule/@tcl/@tk Copilot chat, 25+ commands |
| Neovim | Config snippet (Lua) | Copy tcl_lsp.lua to ~/.config/nvim/lsp/ |
Zero-plugin on 0.11+; also supports nvim-lspconfig |
| Zed | Full extension (TOML + Rust) | Install from Zed extension registry | 16 built-in snippets, MCP context server, /tcl-doc and /irule-event slash commands |
| Emacs | Config snippet (Elisp) | Add to init.el for eglot or lsp-mode |
Works with built-in eglot (Emacs 29+) |
| Helix | Config snippet (TOML) | Add to ~/.config/helix/languages.toml |
Minimal pure-TOML setup |
| Sublime Text | Full package (.sublime-package) | Package Control or manual install | Works standalone (syntax + snippets) without LSP; enhanced with LSP package |
| JetBrains | Full plugin (.zip) | Settings > Plugins > Install from Disk | Compiler explorer tool window, settings UI panel, IntelliJ IDEA 2024.1+ |
All editors connect to the same Python LSP server over stdio. The server can
be invoked from source (uv run python -m server) or as a standalone zipapp
(python3 tcl-lsp-server.pyz).
File types recognised: .tcl, .tk, .itcl, .tm, .irul, .irule,
.iapp, .iappimpl, .impl, .apl, .exp, plus shebang detection for
#!/usr/bin/tclsh, #!/usr/bin/wish, and #!/usr/bin/expect.
Files named presentation (no extension) are auto-detected as APL.
The full-featured extension, distributed as a .vsix, bundles the LSP server
and provides the richest integration.
25+ commands including: Restart Server, Select Dialect, Apply Safe Quick
Fixes, Apply All Optimisations, Open Compiler Explorer, Open Tk Preview,
Format Document, Minify Document, Insert iRule Event Skeleton, Scaffold Tcl
Package Starter, Insert package require, Run Runtime Validation, Translate
iRule to F5 XC, Extract iRule from Config, Escape/Unescape Selection, Base64
Encode/Decode Selection.
Keyboard shortcuts: Ctrl+Alt+O (optimise), Ctrl+Alt+M (minify), Ctrl+Alt+E (compiler explorer).
Status bar: shows the active dialect (clickable to change) and the extension version.
# Install from release
code --install-extension tcl-lsp-0.1.0.vsixZero-plugin setup on Neovim 0.11+ using the native LSP client. Also works
with nvim-lspconfig (0.8+) or a manual FileType autocommand.
-- ~/.config/nvim/lsp/tcl_lsp.lua (Neovim 0.11+)
return {
cmd = { "python3", "/path/to/tcl-lsp-server.pyz" },
filetypes = { "tcl" },
settings = {
tclLsp = {
dialect = "tcl8.6",
formatting = { indentSize = 4, maxLineLength = 120 },
},
},
}
-- init.lua
vim.filetype.add({ extension = { tcl = "tcl", irul = "tcl", irule = "tcl" } })
vim.lsp.enable("tcl_lsp")A full Zed extension that auto-downloads the server zipapp from GitHub Releases on first use and auto-discovers Python 3.10+ on your PATH.
Includes 16 built-in snippets (tcl-proc, tcl-namespace, tcl-if,
irule-http-request, irule-collect-release, etc.), an MCP context server
exposing all 23 analysis tools, and slash commands (/tcl-doc, /irule-event,
/tcl-validate).
# Install from the Zed extension registry (search "Tcl")
# Or install from a release artifact (no Rust required):
# 1. Download tcl-lsp-zed-*.zip from the GitHub Releases page
# 2. unzip tcl-lsp-zed-*.zip -d /tmp/tcl-lsp-zed
# 3. Command Palette (Cmd+Shift+P) > "zed: install dev extension" > select /tmp/tcl-lsp-zed
# Or build from source (requires Rust via rustup — https://rustup.rs):
# 1. make zed
# 2. Command Palette > "zed: install dev extension" > select editors/zed/Works with the built-in eglot client (Emacs 29+) or lsp-mode.
;; eglot (Emacs 29+)
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(tcl-mode . ("python3" "/path/to/tcl-lsp-server.pyz"))))
(add-hook 'tcl-mode-hook #'eglot-ensure)
;; Settings
(setq-default eglot-workspace-configuration
'(:tclLsp (:dialect "tcl8.6"
:formatting (:indentSize 4 :maxLineLength 120))))Minimal TOML configuration in ~/.config/helix/languages.toml.
[language-server.tcl-lsp]
command = "python3"
args = ["/path/to/tcl-lsp-server.pyz"]
[language-server.tcl-lsp.config.tclLsp]
dialect = "tcl8.6"
[[language]]
name = "tcl"
scope = "source.tcl"
file-types = ["tcl", "tk", "itcl", "tm", "irul", "irule", "iapp"]
language-servers = ["tcl-lsp"]A full Sublime Text package (.sublime-package) that works in two modes:
standalone (syntax highlighting + 16 snippets + static completions) and
enhanced (full LSP features when the LSP package is installed).
Auto-discovers the bundled .pyz server from the package archive.
# Install via Package Control:
# Command Palette > Package Control: Install Package > Tcl
# Or manual install:
# Download Tcl.sublime-package from Releases
# Place in ~/Library/Application Support/Sublime Text/Installed Packages/Commands: Select Dialect, Restart Language Server, Format Document, Minify Document, Apply Safe Quick Fixes, Apply All Optimisations.
A full IntelliJ Platform plugin (.zip) for IntelliJ IDEA 2024.1+ and other
JetBrains IDEs. Includes a dedicated settings panel (Settings > Tools > Tcl
Language Server) with toggles for every feature, diagnostic code, and
formatting option.
Features a Compiler Explorer tool window with JCEF browser for inspecting IR, CFG, SSA, and optimiser output directly inside the IDE.
# Install from release:
# Settings > Plugins > ⚙️ > Install Plugin from Disk > tcl-lsp-jetbrains.zip
# Build from source:
make jetbrainsFast syntax feedback fires immediately on every keystroke; deeper semantic, optimiser, and security analysis runs in the background and merges results as each tier completes.
# Tier 1 (instant): syntax errors — missing brace caught on parse
proc broken {x {
puts $x
}
# Tier 2 (background): semantic — arity mismatch flagged after analysis
string length "a" "b" ;# E003: too many argumentsVariables, procs, keywords, and strings are classified using SSA-informed type information, giving richer highlighting than a TextMate grammar alone. The server provides 43 token types beyond the standard LSP set, including sub-token highlighting inside strings.
namespace eval app {
variable count 0 ;# 'count' highlighted as variable
proc handle {request} { ;# 'handle' highlighted as function
incr count ;# 'incr' highlighted as keyword
puts "req: $request" ;# '$request' highlighted as variable inside string
}
}In addition to standard token types (keyword, function, variable, string, comment, number, operator, parameter, namespace), the server provides domain-specific token types:
| Category | Token types | Example |
|---|---|---|
| Regexp | regexpGroup, regexpCharClass, regexpQuantifier, regexpAnchor, regexpEscape, regexpBackref, regexpAlternation |
regexp {(\d+)\s+(\w+)} $line — each part gets distinct highlighting |
| Format strings | formatPercent, formatSpec, formatFlag, formatWidth |
format "%- 10.2f" $val — %, -, 10.2, and f each highlighted |
| Binary format | binarySpec, binaryCount, binaryFlag |
binary scan $data su3 x y z — s, u, and 3 each highlighted |
| Clock format | clockPercent, clockSpec, clockModifier |
clock format $t -format "%Y-%m-%d" — %, Y, m, d each highlighted |
| Escape sequences | escape |
puts "line1\nline2\t${var}" — \n, \t highlighted inside strings |
| BIG-IP config | object, ipAddress, port, partition, pool, monitor, profile, vlan, fqdn, routeDomain, encrypted, interface |
BIG-IP .conf files get object-aware highlighting |
Arity errors, unknown subcommands, best-practice violations, and security
issues are reported with precise ranges. All diagnostics support inline
suppression with # noqa: CODE comments.
string frobulate $x ;# W001: unknown subcommand 'frobulate'
set y [expr $a + $b] ;# W100: unbraced expr (double-substitution risk)
eval $user_input ;# W101: eval with substituted arguments (injection risk)
catch { error "oops" } ;# W302: catch without a result variableContext-aware completions for commands, subcommands, variables, proc names
(workspace-wide), switch arms, and package require names.
string len| ;# offers: length, last, ...
set name "world"
puts $na| ;# offers: $name
dict | ;# offers: create, get, set, exists, ...Hovering on a command, proc call, variable, or operator shows its signature,
doc comment, and type information. Multi-line docstrings are supported, and
@param, @return, and @brief tags are parsed and displayed as structured
markdown. Docstrings can appear above the proc or inside the proc body.
# @brief Greet a person by name.
# @param name - Who to greet
# @return The greeting string
proc greet {name} {
return "Hello, $name!"
}
greet "Alice" ;# hover on 'greet' shows signature + formatted @param/@return docsJump to the definition of a proc or variable — works across files in the workspace.
proc helper {} { return 42 }
set x [helper] ;# Ctrl+Click on 'helper' → jumps to proc definition above
puts $x ;# Ctrl+Click on '$x' → jumps to the set statementLocate every usage of a proc or variable, including inside nested braced
script bodies such as if, foreach, and namespace eval.
proc add {a b} { expr {$a + $b} }
set sum [add 1 2] ;# ← reference to 'add'
puts [add 3 4] ;# ← reference to 'add'
# "Find all references" on 'add' highlights all three locationsInspect incoming callers and outgoing callees for any procedure.
proc validate {input} { return [string is integer $input] }
proc process {data} { if {[validate $data]} { store $data } }
proc main {} { process "42" }
# Incoming calls to 'validate': process
# Outgoing calls from 'process': validate, storeSafely rename a proc or variable across all scopes in the file.
proc greeting {name} {
puts "Hi, $name"
}
greeting "World"
# Rename 'greeting' → 'salute' updates the proc definition and all call sitesAs you type arguments, the server shows the expected parameter list with the active parameter highlighted.
proc connect {host port {timeout 30}} { ... }
connect "db.local" |
# ↑ signature help shows: connect (host port ?timeout?)
# with 'port' highlighted as the active parameterInline annotations show inferred types, format-string specifier meanings, and parameter names.
set count 42 ;# inlay: ': int'
set msg [format "%s has %d items" $name $count]
# ↑ '%s → string' ↑ '%d → integer'A structured outline of the current file — procs, namespaces, variables — for quick navigation (Ctrl+Shift+O / Cmd+Shift+O).
namespace eval app {
variable config {} ;# symbol: app::config (variable)
proc init {} { ... } ;# symbol: app::init (function)
proc run {} { init } ;# symbol: app::run (function)
}
# Outline: app (namespace) → config, init, runSearch for procs and variables across all open files in the workspace (Ctrl+T / Cmd+T).
# File: utils.tcl
proc ::utils::parse_csv {data} { ... }
# File: main.tcl
# Type "parse_csv" in workspace symbol search → jumps to utils.tclCollapse proc bodies, control-flow blocks, multi-line comments, and namespace bodies.
# ── Header comment ── ← foldable
# Author: ...
proc calculate {x} { ← foldable
if {$x > 0} { ← foldable
return [expr {$x * 2}]
}
}Smart expand/shrink selection by syntactic structure (Alt+Shift+→ / Alt+Shift+←).
proc greet {name} {
puts "Hello $name"
}
# Cursor on 'name' inside puts → expand: "$name" → "Hello $name" → puts command → proc body → proc → filesource paths and package require names become clickable links that
navigate to the resolved file or package.
package require http ;# click → opens http package source
source lib/utils.tcl ;# click → opens lib/utils.tclFull-document and range formatting with 24 configurable options. Defaults
follow the F5 iRules Style Guide. Supports full-document
(textDocument/formatting) and range (textDocument/rangeFormatting)
requests.
# Before:
proc messy { x } {
if {$x>0}{return $x }
set y [expr $x+1] ; puts $y }
# After (formatted):
proc messy {x} {
if {$x > 0} {
return $x
}
set y [expr $x + 1]
puts $y
}Capabilities include indentation (spaces or tabs, configurable size),
brace placement (K&R), expression bracing enforcement, variable
bracing ($var → ${var}), line-length wrapping, semicolon splitting,
single-line body expansion, blank-line normalisation between procs and
blocks, comment alignment, trailing whitespace trimming, and line-ending
normalisation (LF/CRLF/CR).
# Expression bracing enforcement (enforceBracedExpr = true):
if {$x > 0} { ... } ;# ✓ braced
if $x>0 { ... } ;# → rewritten to: if {$x > 0} { ... }
# Variable bracing (enforceBracedVariables = true):
puts $name ;# → rewritten to: puts ${name}Quick-fix actions are offered for diagnostics that have automated repairs. Refactoring actions are available on selected code.
expr $a + $b ;# W100 → quick-fix: wrap in braces → expr {$a + $b}
catch { error "x" } ;# W302 → quick-fix: add result variable → catch { error "x" } result
set x [expr {$x+1}] ;# O114 → quick-fix: use incr idiom → incr xExtract to proc — select one or more lines, trigger code actions
(Ctrl+.), and choose Extract selection into proc. The selected code
moves into a new proc with detected variable parameters; the original
lines are replaced with a call. The editor places the cursor on the new
proc name so you can rename it immediately.
Bundled code templates for Tcl structures and iRules event skeletons with secure defaults, collect/release pairs, and common patterns.
# Type 'proc' + Tab:
proc name {args} {
# body
}
# Type 'when' + Tab (iRules):
when HTTP_REQUEST {
# handler
}Switch between Tcl 8.4/8.5/8.6/9.0, F5 iRules, F5 iApps, and EDA tooling
profiles. Tk, tcllib, and stdlib commands activate automatically when their
package require appears. F5 iRules metadata follows BIG-IP command/event
source data, including profile aliases used by newer namespaces and events,
shared TLS helper profiles such as PERSIST, and protocol namespace layer
metadata that stays aligned with the enabling profile stack.
# With dialect = tcl8.6:
try {
open $path r ;# ✓ known in 8.6
} on error {msg} {
puts $msg
}
# With dialect = tcl8.5:
try { ... } ;# W002: command disabled in active dialect (try requires 8.6)The 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.
proc fibonacci {n} {
set a 0; set b 1
for {set i 0} {$i < $n} {incr i} {
set t $b
set b [expr {$a + $b}]
set a $t
}
return $a
}
# IR → CFG → SSA → SCCP → liveness → type inference → bytecodeTwenty-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.
# O102 — constant expression folding:
set a 1
set b [expr {$a + 2}] ;# → suggestion: replace with 'set b 3'
# O114 — incr idiom recognition:
set x [expr {$x + 1}] ;# → suggestion: replace with 'incr x'
# O105 — constant var-ref propagation / redundant computation (GVN/CSE):
set a [expr {$x + $y}]
set b [expr {$x + $y}] ;# → suggestion: replace with 'set b $a'
# O106 — loop-invariant code motion (LICM):
for {set i 0} {$i < $n} {incr i} {
set len [string length $fixed] ;# → suggestion: hoist above the loop
lappend result $len
}Tracks 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.
# S100 — single shimmer (info):
set x "hello"
set n [llength $x] ;# 'x' shimmers from STRING → LIST
# S101 — shimmer inside loop (warning):
set s "42"
for {set i 0} {$i < 1000} {incr i} {
set v [expr {$s + $i}] ;# 's' shimmers STRING → INT on every iteration
}
# S102 — type thunking (warning):
for {set i 0} {$i < 100} {incr i} {
set n [llength $data] ;# 'data' shimmers STRING → LIST
set data "updated $n" ;# 'data' back to STRING — oscillates each iteration
}Colour-aware data provenance tracking follows untrusted I/O through
assignments, interpolation, and phi nodes to dangerous sinks. Commands that
produce fixed-type results (e.g. string length) act as sanitisers.
# T100 — tainted data in dangerous sink:
set input [gets stdin]
eval $input ;# ✗ tainted data flows into eval
# T102 — tainted data in option position:
set pat [HTTP::uri]
regexp $pat $string ;# ✗ tainted pattern without '--' terminator
regexp -- $pat $string ;# ✓ safe: '--' prevents option injection
# IRULE1007 — collect without release (side-aware):
when HTTP_REQUEST {
HTTP::collect 1048576 ;# ✗ missing matching HTTP::release on client side
}Call graph, symbol graph, and data-flow graph are exposed for AI agent consumption — enabling automated code review, impact analysis, and refactoring assistance.
proc validate {input} { string is integer $input }
proc store {data} { puts $data }
proc process {x} { if {[validate $x]} { store $x } }
# Call graph query: "who calls validate?" → process
# Symbol graph query: "variables in process" → x
# Data-flow query: "trace $x" → parameter → validate → storeAn interactive webview panel (Ctrl+Alt+E / Cmd+Alt+E) that visualises the compiler's intermediate representation, control-flow graph, SSA form, and optimiser output for the active editor.
┌─────────────────────────────────────────────────┐
│ IR │ CFG │ SSA │ Optimiser │ Bytecode │
├─────────────────────────────────────────────────┤
│ proc fibonacci {n} │
│ ENTRY: │
│ %0 = param n │
│ %1 = const 0 ; set a 0 │
│ %2 = const 1 ; set b 1 │
│ LOOP: │
│ %3 = phi [%1, ENTRY] [%6, BODY] │
│ ... │
└─────────────────────────────────────────────────┘
A live preview panel that extracts the widget hierarchy from Tk source and renders a visual layout — updates in real time as you edit.
package require Tk
ttk::frame .f
ttk::label .f.lbl -text "Name:"
ttk::entry .f.ent -textvariable name
ttk::button .f.btn -text "OK" -command { puts $name }
grid .f.lbl .f.ent .f.btn -padx 5 -pady 5
pack .f
# Preview panel shows the grid layout with label, entry, and buttonOpen a BIG-IP .conf or .scf file to get syntax highlighting, object
navigation, and iRule extraction.
# BIG-IP config file (bigip.conf)
ltm virtual /Common/my_vs {
destination /Common/10.0.0.1:443
pool /Common/my_pool
rules {
/Common/my_irule ← right-click → "Open iRule in Editor"
}
}
# "Extract All iRules to Files..." exports every iRule to separate .tcl files
Open .apl files or files named presentation to get semantic highlighting
for the iApp Application Presentation Language. APL-specific tokens include
section/table/row keywords, field types (string, choice, password, ...),
attributes (default, display, required, validator), define blocks,
optional conditionals, #include/#inline directives, and validator names.
Embedded Tcl inside [...] brackets (e.g. [tmsh::get_config ...]) receives
full Tcl semantic highlighting.
# iApp APL presentation file
section basic {
string addr default "0.0.0.0" required validator "IpAddress"
choice protocol display "medium" default "tcp" {
"TCP" => "tcp",
"UDP" => "udp"
}
yesno use_snat default "yes"
}
text {
basic "Basic Configuration"
basic.addr "Virtual Server IP Address"
}
Cross-file integration: When a presentation (APL) file and an
implementation (iApp Tcl) file are in the same directory, the server
cross-validates them:
- IAPP7001: Implementation references a variable (
$::section__field) not defined in the presentation - IAPP7002: Presentation field is never referenced in the implementation
- IAPP7003:
#includefile not found
The #include directive is resolved relative to the APL file's directory,
with recursive resolution and circular-include protection.
The f5-iapps dialect includes 30+ tmsh:: namespace commands
(tmsh::create, tmsh::modify, tmsh::get_config, tmsh::get_field_value,
etc.) and 4 script:: commands (script::run, script::init, etc.) with
hover documentation and arity validation.
Translate F5 BIG-IP iRules to F5 Distributed Cloud configuration, with both Terraform HCL and JSON API output plus a coverage report.
# Source iRule:
when HTTP_REQUEST {
if { [HTTP::uri] starts_with "/api" } {
pool api_pool
} else {
HTTP::redirect "https://[HTTP::host]/api[HTTP::uri]"
}
}
# "Translate iRule to F5 XC" produces:
# - Terraform HCL with routes, origin pools, and redirect rules
# - JSON API payload for direct XC API calls
# - Coverage report showing translated vs. untranslatable constructsGenerate and run deterministic tests for F5 iRules. The framework simulates
BIG-IP's event lifecycle, pool selection, data groups, and multi-TMM CMP
behaviour in a standard tclsh.
::orch::configure_tests \
-profiles {TCP HTTP} \
-irule { when HTTP_REQUEST { pool web_pool } } \
-setup { ::orch::add_pool web_pool {{10.0.0.1:80}} }
::orch::test "routing-1.0" "basic request goes to web_pool" -body {
::orch::run_http_request -host "example.com" -uri "/"
::orch::assert_that pool_selected equals "web_pool"
}
exit [::orch::done]The generate-test CLI command and generate_irule_test MCP tool analyse an
iRule's control-flow graph to produce test cases automatically. For iRules
with CMP-sensitive patterns (static:: writes in hot events, table shared
state), multi-TMM scenarios using fakeCMP distribution are included.
Optionally run the active file through a real tclsh (or an iRules stub
adapter) on save to catch issues that static analysis alone cannot detect.
# With tclLsp.runtimeValidation.enabled = true:
proc test {} {
package require NoSuchPackage ;# runtime error: can't find package
}
# The server invokes tclsh in syntax-check mode and merges runtime
# errors into the diagnostics panel alongside static analysis resultsEditor commands for common encoding operations, available from the right-click context menu or the command palette.
Escape Selection → converts special chars to Tcl backslash sequences
Unescape Selection → reverses backslash sequences to literal chars
Base64 Encode Selection → encodes selected text as base64
Base64 Decode Selection → decodes base64 back to text
Copy File as Base64 → copies entire file content as base64 to clipboard
Copy File as Gzip+Base64 → compresses then base64-encodes file to clipboard
Generate a complete Tcl package project layout with a single command.
"Tcl: Scaffold Tcl Package Starter" creates:
mypackage/
pkgIndex.tcl Package index
mypackage.tcl Package source with namespace and public API
tests/
all.tcl Test runner
mypackage.test tcltest skeleton
.github/
workflows/ci.yml GitHub Actions CI workflow
README.md Package README
Three chat participants integrate with GitHub Copilot to provide domain-specific AI assistance backed by the LSP's static analysis.
| Command | Description |
|---|---|
/create |
Generate a new iRule from a natural-language description |
/explain |
Explain what an iRule does, including data flow and security |
/fix |
Iteratively fix all LSP diagnostics in the current iRule |
/validate |
Run full LSP validation and show a categorised report |
/review |
Deep security and safety review (injection, DoS, races) |
/convert |
Modernise legacy patterns (unbraced expr, matchclass, etc.) |
/optimise |
Apply optimiser suggestions with explanations |
/scaffold |
Generate an iRule skeleton from selected events |
/datagroup |
Suggest data-group extraction for inline lookups |
/diff |
Explain differences between two iRule versions |
/event |
Show which commands are valid in a given event |
/migrate |
Convert nginx/Apache/HAProxy config to an iRule |
/diagram |
Generate a Mermaid flowchart of the iRule's logic flow |
/xc |
Translate the iRule to F5 Distributed Cloud configuration |
User: @irule /create rate limiter that allows 100 requests per minute per client IP
Copilot: generates a complete iRule with HTTP_REQUEST handler, table-based
counting, and HTTP::respond 429 — validated against the LSP
| Command | Description |
|---|---|
/create |
Generate Tcl code from a description |
/explain |
Explain what Tcl code does |
/fix |
Iteratively fix all LSP diagnostics |
/validate |
Run full LSP validation and show a report |
/optimise |
Apply optimiser suggestions with explanations |
User: @tcl /explain what does the fibonacci proc do?
Copilot: walks through the loop, variable assignments, and return value
| Command | Description |
|---|---|
/create |
Generate a Tk GUI from a description |
/explain |
Explain the widget hierarchy and layout |
/preview |
Open the Tk Preview pane for the current file |
User: @tk /create a simple calculator with number buttons and a display
Copilot: generates Tk code with grid layout, button callbacks, and display label
Twenty purpose-built skills for Claude Code (CLI) that combine LSP static
analysis with AI reasoning. Each skill invokes the tcl-lsp-ai analyser,
iterates on diagnostics, and produces clean output.
| Skill | Description |
|---|---|
irule-create |
Generate a new iRule from a description, validate until clean |
irule-explain |
Explain an iRule's logic, data flow, and security posture |
irule-fix |
Iteratively fix all diagnostics (analyse → fix → re-analyse) |
irule-validate |
Categorised validation report (errors, security, style, optimiser) |
irule-review |
Deep security audit: injection, DoS, races, information leakage |
irule-convert |
Modernise legacy patterns to current best practices |
irule-optimise |
Apply optimiser suggestions with safety explanations |
irule-scaffold |
Generate event skeleton with log gating and placeholders |
irule-datagroup |
Suggest data-group extraction for inline lookups |
irule-diff |
Explain semantic differences between two iRule versions |
irule-event |
Look up event/command validity from the registry |
irule-migrate |
Convert nginx/Apache/HAProxy config to an iRule |
irule-diagram |
Generate a Mermaid flowchart from compiler IR |
irule-xc |
Translate to F5 XC with Terraform and JSON output |
tcl-create |
Generate Tcl code from a description, validate until clean |
tcl-explain |
Explain Tcl code with analysis context |
tcl-fix |
Iteratively fix all Tcl diagnostics |
tcl-validate |
Categorised Tcl validation report |
tcl-optimise |
Apply Tcl optimiser suggestions |
tk-create |
Generate Tk GUI code with proper widget hierarchy |
# Example: fix all issues in an iRule
claude /irule-fix my_irule.tcl
# Example: security review
claude /irule-review production_rule.tcl
# Example: generate a Mermaid diagram
claude /irule-diagram complex_rule.tclA Model Context Protocol server that exposes tcl-lsp analysis as 27 tools for any MCP-compatible client (Claude Desktop, custom agents, etc.).
| Tool | Description |
|---|---|
analyze |
Full analysis: diagnostics, symbols, events, and metadata |
validate |
Categorised validation report |
review |
Security-focused diagnostic report |
convert |
Detect legacy patterns for modernisation |
optimize |
Optimisation suggestions with rewritten source |
hover |
Hover information at a position |
complete |
Completions at a position |
goto_definition |
Find definition of a symbol |
find_references |
Find all references to a symbol |
symbols |
Document symbol hierarchy |
code_actions |
Quick fixes for a source range |
format_source |
Format Tcl/iRules source code |
rename |
Rename a symbol throughout the document |
event_info |
iRules event metadata and valid commands |
command_info |
Command metadata and valid events |
event_order |
Events in canonical firing order |
call_graph |
Build proc call graph with roots and leaves |
symbol_graph |
Build scope/definition/reference graph |
dataflow_graph |
Build taint and side-effect graph |
diagram |
Extract control-flow diagram data from IR |
xc_translate |
Translate iRule to XC configuration |
tk_layout |
Extract Tk widget tree as JSON |
generate_irule_test |
Generate iRule test script with CFG paths and multi-TMM detection |
irule_cfg_paths |
Extract CFG control-flow paths for test planning |
fakecmp_which_tmm |
Look up which TMM a connection tuple maps to |
fakecmp_suggest_sources |
Find client addr/port combos that hit each TMM |
set_dialect |
Set active Tcl dialect for the session |
// Claude Desktop — claude_desktop_config.json
{
"mcpServers": {
"tcl-lsp": {
"command": "./tcl-lsp-mcp-server.pyz"
}
}
}All CLI tools are distributed as self-contained Python zipapps (.pyz) — no
pip install required.
A single verb-based CLI that aggregates common local workflows:
opt/optimise— optimise combined input source and emit rewritten Tcldiag— run diagnostics across files/directories/packageslint— run lint diagnostics across files/directories/packagesvalidate— error-level validation checksformat— format source using canonical Tcl style rulessymbols— emit symbol definitions for the resolved sourcediagram— extract control-flow diagram data from compiler IRcallgraph— build procedure call graph datasymbolgraph— build symbol relationship graph datadataflow— build taint/effect data-flow graph dataevent-order— show iRules events in canonical firing orderevent-info— look up iRules event metadata and valid commandscommand-info— look up command registry metadataconvert— detect legacy modernisation patternsdis— bytecode disassemblycompwasm— compile input to a WASM binaryhighlight— emit syntax-highlighted source (ansiorhtml)diff— compare two sources across AST/IR/CFG compiler representationsexplore— run compiler-explorer views (ir,cfg,ssa,opt,asm,wasm, ...)help— search bundled KCS feature docs from the SQLite help index
# Optimise everything under src/ into one output script
python tcl.pyz opt src/ -o build/optimised.tcl
# Run diagnostics across a directory and a Tcl package
python tcl.pyz diag src/ mypkg --package-path ./vendor/tcl
# Run lint diagnostics (same checks as `diag`)
python tcl.pyz lint src/ mypkg --package-path ./vendor/tcl
# Validate syntax/error diagnostics
python tcl.pyz validate src/
# Validate as JSON
python tcl.pyz validate src/ --json
# Format source text
python tcl.pyz format script.tcl -o formatted.tcl
# Minify source (strip comments, collapse whitespace, join commands)
python tcl.pyz minify script.tcl -o minified.tcl
# Aggressive minify (optimise + static substring folding via SCCP + name compaction)
python tcl.pyz minify --aggressive script.tcl -o minified.tcl --symbol-map map.txt
# Symbol/graph/event/convert analysis verbs
python tcl.pyz symbols script.tcl --json
python tcl.pyz diagram script.tcl --json
python tcl.pyz callgraph script.tcl --json
python tcl.pyz symbolgraph script.tcl --json
python tcl.pyz dataflow script.tcl --json
python tcl.pyz event-order rule.irule --dialect f5-irules --json
python tcl.pyz event-info HTTP_REQUEST --json
python tcl.pyz command-info HTTP::uri --dialect f5-irules --json
python tcl.pyz convert rule.irule --json
# Emit bytecode disassembly
python tcl.pyz dis script.tcl
# Compile to WASM binary (+ optional WAT sidecar)
python tcl.pyz compwasm script.tcl -o out.wasm --wat-output out.wat
# Emit ANSI-highlighted output (or --format html)
python tcl.pyz highlight script.tcl --force-colour
# Diff two iRules using compiler structure layers
python tcl.pyz diff old.irule new.irule --show ast,ir,cfg
# Use compiler explorer views from the same zipapp
python tcl.pyz explore script.tcl --show ir,cfg,opt
# Search KCS help docs (optionally scoped by dialect)
python tcl.pyz help taint analysis --dialect f5-irules
# Show help for the help command itself
python tcl.pyz help --help
# Emit help search results as JSON
python tcl.pyz help taint --jsonYou can symlink the same zipapp as irule:
ln -sf ./tcl.pyz ./irule
./irule lint rules/When invoked as irule, the CLI uses f5-irules as the default dialect.
For source builds, run make kcs-db before packaging zipapps so tcl.pyz help
can query the bundled KCS SQLite database.
Console tool for inspecting the compiler pipeline: IR, CFG, SSA, optimiser rewrites, shimmer warnings, taint analysis, and bytecode.
# Full exploration of a Tcl file
uv run python -m explorer script.tcl
# Focus on optimiser rewrites only
uv run python -m explorer script.tcl --show opt
# Inline source with optimised output
uv run python -m explorer --source 'set a 1; set b [expr {$a + 2}]' --show-optimised-source
# Show only IR and CFG
uv run python -m explorer script.tcl --show ir,cfg
# iRules dialect with flow analysis
uv run python -m explorer irule.tcl --dialect bigip --show irulesAvailable views: ir, cfg, ssa, interproc, types, opt, gvn,
shimmer, taint, irules, callouts, asm, wasm. Groups: all,
compiler, optimiser.
Standalone static analyser for use with AI agents and CI pipelines.
# Full context pack (diagnostics + symbols + events) as JSON
uv run python -m ai.claude.tcl_ai context script.tcl
# Categorised validation report
uv run python -m ai.claude.tcl_ai validate script.tcl
# Security-focused review
uv run python -m ai.claude.tcl_ai review irule.tcl
# Optimisation suggestions with rewritten source
uv run python -m ai.claude.tcl_ai optimize script.tcl
# Build call graph
uv run python -m ai.claude.tcl_ai call-graph script.tcl
# Look up iRules event metadata
uv run python -m ai.claude.tcl_ai event-info HTTP_REQUEST
# Extract Tk widget tree
uv run python -m ai.claude.tcl_ai tk-layout gui.tcl
# Generate iRule test script (Event Orchestrator framework)
uv run python -m ai.claude.tcl_ai generate-test irule.tcl
# Extract CFG paths for test planning
uv run python -m ai.claude.tcl_ai cfg-paths irule.tclCompile Tcl scripts to WebAssembly (WAT text or binary WASM format).
# Compile to human-readable WAT
uv run python -m explorer.wasm_cli script.tcl --format wat
# Compile to WASM binary with optimisations
uv run python -m explorer.wasm_cli script.tcl -O --format wasm -o out.wasm
# Compare optimised vs. unoptimised output
uv run python -m explorer.wasm_cli --source 'set x [expr {1+2}]' --format bothA standalone web UI for the compiler explorer, available in two variants: offline (bundles Pyodide) and CDN (loads Pyodide from jsDelivr).
# Standalone (offline, ~100 MB)
./tcl-lsp-explorer-gui.pyz --port 8080
# CDN variant (lightweight, requires internet)
./tcl-lsp-explorer-gui-cdn.pyz --port 8080A bytecode interpreter that compiles and executes Tcl scripts using the compiler pipeline, with an interactive REPL and disassembly mode.
# Execute a script
uv run python -m vm script.tcl arg1 arg2
# Interactive REPL
uv run python -m vm
# Inline evaluation
uv run python -m vm -e 'puts [expr {6 * 7}]'
# Show bytecode disassembly without executing
uv run python -m vm --disassemble script.tclAn interactive debugger that can single-step through Tcl scripts with breakpoints, variable inspection, and call stack visualisation. Three backends are available:
| Backend | Description |
|---|---|
vm |
The project's own bytecode VM (default) |
tclsh |
External tclsh subprocess |
tkinter |
Python's built-in tkinter.Tcl() interpreter |
# Debug a script (uses VM backend by default)
uv run python -m debugger script.tcl
# Force a specific backend
uv run python -m debugger --backend vm script.tcl
# Read from stdin
echo 'puts hello' | uv run python -m debugger -Debugger commands: run, step/s, next/n, finish, continue/c,
break <line>/b, delete <id>/d, vars, print <var>/p, stack,
list/l, quit/q.
The 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.
| Dialect | Description |
|---|---|
tcl8.4 |
Tcl 8.4 core commands |
tcl8.5 |
Tcl 8.5 core commands (adds {*}, lassign, dict, etc.) |
tcl8.6 |
Tcl 8.6 core commands (adds try/finally, tailcall, coroutines) -- default |
tcl9.0 |
Tcl 9.0 core commands (adds lpop, zipfs, updated encoding) |
f5-irules |
F5 BIG-IP iRules: HTTP/SSL/DNS/LB namespaces, event-validity checks, taint analysis, static:: scoping rules |
f5-iapps |
F5 iApps template commands |
f5-bigip |
F5 BIG-IP configuration (bigip.conf) commands |
synopsys-eda-tcl |
Synopsys EDA commands (Design Compiler, PrimeTime, ICC2, Formality) |
cadence-eda-tcl |
Cadence EDA commands (Genus, Innovus, Tempus, Xcelium) |
xilinx-eda-tcl |
Xilinx/AMD EDA commands (Vivado, Vitis) |
intel-quartus-eda-tcl |
Intel Quartus Prime commands |
mentor-eda-tcl |
Mentor/Siemens EDA commands (ModelSim, Questa, Calibre) |
expect |
Expect: spawn, expect, send, interact and related commands for automating interactive programs |
Tk, tcllib, and Tcl stdlib commands are automatically recognised
when the corresponding package require appears in the file. No manual
toggle is needed — the registry activates the relevant command definitions
per-document.
For 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:
External stub files (<name>.tcl.stubs):
# synopsys.tcl.stubs
stub foreach_in_collection {varName:var collection body:body} -loop
stub get_cells {?-hierarchical? ?-filter? pattern:pattern} -pure
stub sizeof_collection {collection} -pure
stub expr-func sizeof 1
Inline stubs (in any .tcl file, using markers):
# tcl-lsp: stubs-begin
# tcl-lsp: stub foreach_in_collection {varName:var collection body:body} -loop
# tcl-lsp: stub get_cells {pattern:pattern} -pure
# tcl-lsp: stub expr-func sizeof 1
# tcl-lsp: stub expr-op contains 2
# tcl-lsp: stubs-endMultiple stubs blocks per file are supported. Argument roles include
body, expr, var, var_read, name, pattern, channel, and
value (default). Flags include -barrier, -loop, -pure,
-mutator, -unsafe, and -scope_alias.
Expression stubs declare custom math functions (expr-func) and infix
operators (expr-op) with optional arity.
See KCS: Dialect stubs for full syntax.
The analyser automatically infers how each proc parameter is used inside the proc body, producing structured trait annotations:
| Trait | Detected pattern |
|---|---|
EVAL |
eval $param, uplevel 1 $param |
BODY |
foreach item $list $param |
VAR_WRITE |
upvar 1 $param local; set local 42 |
VAR_READ |
upvar 1 $param local; return $local |
EXPR |
if {$param} {...} |
LOOP_LIST |
foreach item $param {...} |
Two 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.
See KCS: Proc arg traits for details.
Tcl: Insert Tcl Template Snippet-- quick-pick and insert any bundled Tcl/iRules snippet template.Tcl: Insert iRule Event Skeleton-- scaffold selected iRules events into a new Tcl buffer.Tcl: Scaffold Tcl Package Starter-- generate package layout, tests, CI workflow, and README.Tcl: Insert package require-- suggest and insertpackage requirelines based on symbol usage.Tcl: Apply Safe Quick Fixes-- apply all non-overlapping safe quick fixes in one pass.Tcl: Run Runtime Validation-- run dialect-aware runtime checks on demand.
The formatter supports full-document and range formatting via the standard LSP
textDocument/formatting and textDocument/rangeFormatting requests. Defaults
follow the F5 iRules Style Guide.
Capabilities include:
- 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}instead ofexpr $x + 1 - Variable bracing -- optionally rewrite
$varas${var} - Line length -- hard limit and soft goal; long lines are wrapped at continuation points
- Semicolons -- convert
;-separated commands to individual lines - Body expansion -- optionally expand single-line
if/foreach/etc. bodies to multi-line - Blank lines -- normalise spacing between procs, between control-flow blocks, and cap consecutive blank lines
- Comments -- ensure space after
#, 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
The formatter also recognises multi-line docstrings with @param, @return,
and @brief tags (doxygen-style) and displays them as structured hover
information. Body-internal docstrings (comment blocks at the start of a proc
body) are supported as a fallback when no preceding comment exists.
All options are exposed through tclLsp.formatting.* settings (see
Configuration below).
All diagnostics support inline suppression with # noqa: CODE1,CODE2 comments.
Use # noqa: * to suppress all diagnostics on a line.
| Code | Description | Quick-fix |
|---|---|---|
| E001 | Missing required subcommand | |
| E002 | Too few arguments | |
| E003 | Too many arguments | |
| E100 | Unmatched ] -- missing opening [ |
Insert [ |
| E101 | Missing { after switch -- body cases follow without braces |
|
| E102 | Unmatched } -- missing opening { |
Remove stray } |
| E103 | Missing } -- a nested body consumed this closing brace |
|
| E200 | Parse error -- internal representation cannot be determined |
| Code | Description | Quick-fix |
|---|---|---|
| W001 | Unknown subcommand | |
| W002 | Command is disabled in active dialect profile | |
| W100 | Unbraced expr/if/while/for expression (double substitution risk) |
Wrap in braces |
| W104 | append with space-separated values (use lappend for lists) |
|
| W105 | Unbraced code block or missing variable declaration in namespace eval |
Wrap in braces |
| W106 | Dangerous unbraced switch body |
|
| W108 | Non-ASCII characters in token content (smart quotes, non-breaking spaces) | Replace with ASCII |
| W110 | ==/!= on strings in expr (use eq/ne) |
Replace operator |
| W111 | Line exceeds configured maximum length | |
| W112 | Trailing whitespace | Remove whitespace |
| W113 | Procedure shadows a built-in command | |
| W114 | Redundant nested [expr] -- already in expression context |
|
| W115 | Backslash-newline in comment silently swallows the next line | Convert to per-line comments |
| W120 | Package-gated command used without package require |
Insert package require |
| W121 | Subnet mask has non-contiguous bits | Replace with nearest valid mask |
| W122 | Mistyped IPv4 address (octet > 255 or leading zero) | |
| W200 | Binary format modifier requires newer Tcl | |
| W201 | Manual path concatenation (use file join) |
Rewrite as [file join] |
| Code | Description | Quick-fix |
|---|---|---|
| H300 | Possible paste error -- repeated assignment to same variable with same value | |
| W210 | Variable read before set | |
| W211 | Variable set but never used | |
| W212 | Variable substitution where name expected (set $x, incr $x, info exists $x, etc.) |
|
| W213 | unset on variable that may not exist -- use unset -nocomplain |
|
| W214 | Unused proc parameter -- argument declared but never read in the body | |
| W220 | Dead store -- variable set but overwritten before use |
| Code | Description | Quick-fix |
|---|---|---|
| W101 | eval with substituted arguments (code injection risk) |
|
| W102 | subst with a variable argument (template injection risk) |
|
| W103 | open with pipeline or variable argument (command injection risk) |
|
| W300 | source with a variable path (code execution risk) |
|
| W301 | uplevel with unbraced or multi-arg script (injection risk) |
|
| W303 | regexp with nested quantifiers (ReDoS risk) |
|
| W304 | Missing -- on option-bearing commands before positional input |
Insert -- |
| W306 | Substitution in literal-expected argument position | |
| W307 | Non-literal command name (variable or command substitution as command) | |
| W308 | subst without -nocommands |
|
| W309 | eval/uplevel with subst -- double substitution risk |
|
| W310 | Hardcoded credentials (API keys, tokens, passwords) | |
| W311 | Unsafe channel encoding mismatch (-encoding binary with -translation) |
|
| W312 | interp eval/interp invokehidden with dynamic script (injection risk) |
|
| W313 | Destructive file operations (delete/rename/mkdir) with variable path |
| Code | Description | Quick-fix |
|---|---|---|
| W302 | catch without a result variable (silently swallows errors) |
Add result variable |
The 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.
| Code | Severity | Description |
|---|---|---|
| S100 | Info | Single shimmer outside a loop |
| S101 | Warning | Shimmer inside a loop body (per-iteration cost) |
| S102 | Warning | Variable oscillates between two types across loop iterations (type thunking) |
The taint analyser tracks data provenance through the SSA graph using a
colour-aware lattice. Values originating from I/O commands (network reads,
file reads, process execution) are tagged as tainted. Taint propagates
through assignments, string interpolation, and phi nodes. Commands that
produce fixed-type results (e.g. string length, llength) act as
sanitisers.
Taint colours carry value properties (e.g. PATH_PREFIXED for values
starting with /). At join points, colours are intersected so only
properties shared by all paths survive -- this suppresses false positives.
| Code | Severity | Description | Quick-fix |
|---|---|---|---|
| T100 | Warning | Tainted data flows into a dangerous code-execution sink | |
| T101 | Warning | Tainted data flows into an output command | |
| T102 | Warning | Tainted data in option position without -- terminator |
Insert -- |
| T103 | Warning | Tainted data in regexp/regsub pattern (regex injection / ReDoS risk) |
Wrap with [regex::quote] |
| T104 | Warning | Tainted data in network address argument (SSRF risk) | |
| T105 | Warning | Tainted data in interp eval script argument (cross-interpreter injection) |
|
| T106 | Info | Double-encoding -- value already carries encoding colour | Remove redundant encoder |
These diagnostics fire only in the f5-irules dialect.
| Code | Severity | Description | Quick-fix |
|---|---|---|---|
| IRULE1001 | Warning/Hint | Command invalid or ineffective in this iRules event | |
| IRULE1002 | Warning | Unknown iRules event name | |
| IRULE1003 | Warning | Deprecated iRules event | |
| IRULE1004 | Hint | when block missing explicit priority |
|
| IRULE1005 | Warning | *_DATA event handler without matching *::collect call |
Bootstrap collect |
| IRULE1006 | Warning | *::payload access without matching *::collect call |
Bootstrap collect |
| IRULE1007 | Error | *::collect without matching *::release on the same connection side |
|
| IRULE1008 | Error | *::release without matching *::collect on the same connection side |
|
| IRULE1201 | Warning | HTTP command used after HTTP::respond/HTTP::redirect |
|
| IRULE1202 | Warning | Multiple HTTP::respond/HTTP::redirect on different branches |
| Code | Severity | Description | Quick-fix |
|---|---|---|---|
| IRULE2001 | Warning | Deprecated matchclass -- use class match |
Auto-replace |
| IRULE2002 | Warning | Deprecated iRules command | |
| IRULE2003 | Error | Unsafe iRules command (context escalation risk) |
| Code | Severity | Description | Quick-fix |
|---|---|---|---|
| IRULE3001 | Warning | Tainted data in HTTP response body (XSS risk) | Wrap with [HTML::encode] |
| IRULE3002 | Warning | Tainted data in HTTP header or cookie value (header injection) | Wrap with [URI::encode] |
| IRULE3003 | Warning | Tainted data in log command (log injection) |
|
| IRULE3004 | Warning | Tainted data in HTTP::redirect URL (open redirect risk) |
|
| IRULE3101 | Warning | HTTP::uri/HTTP::path set to value not provably starting with / |
|
| IRULE3102 | Warning | HTTP::path/HTTP::uri/HTTP::query getter used without -normalized |
|
| IRULE3103 | Info | *::uri used where *::path or *::query suffices (split, starts_with, contains, string match, etc.) |
| Code | Severity | Description |
|---|---|---|
| IRULE4001 | Warning | Write to static:: variable outside RULE_INIT (race condition) |
| IRULE4002 | Hint | Generic static:: variable name — collision likely across iRules |
| IRULE4003 | Hint | Variable scoping concern across events |
| IRULE4004 | Info | Constant set in per-request event could be hoisted to per-connection |
| IRULE4005 | Warning | Potential race — static:: variable written outside RULE_INIT and read in another event |
| Code | Severity | Description | Quick-fix |
|---|---|---|---|
| IRULE2101 | Hint | Heavy regexp in a high-frequency event |
|
| IRULE5001 | Hint | Ungated log in a high-frequency event |
|
| IRULE5002 | Warning | drop/reject/discard without event disable all or return |
Add event disable all + return |
| IRULE5003 | Hint | Loop condition $var != 0 can miss zero if decremented past it |
|
| IRULE5004 | Warning | DNS::return without return |
Add return |
| IRULE5005 | Error | Direct proc invocation without call in iRules |
Prefix with call |
The optimiser operates on the SSA/CFG intermediate representation and suggests source-level rewrites. All optimiser diagnostics appear at Information severity and include a quick-fix code action with the suggested replacement.
Each pass can be individually toggled via tclLsp.optimiser.* settings.
| Code | Description | Technique |
|---|---|---|
| O100 | Propagate constant variables into expressions and command arguments | SCCP |
| O101 | Fold constant integer expressions | Constant folding |
| O102 | Fold constant [expr {...}] command substitutions |
Constant folding |
| O103 | Fold static procedure calls using interprocedural summaries | Interprocedural analysis |
| O104 | Fold static string-build chains into a single assignment | Copy propagation |
| O105 | Propagate constants into variable references; detect redundant computations (GVN/CSE + PRE) | Constant propagation, global value numbering |
| O106 | Hoist loop-invariant computations | LICM |
| O107 | Eliminate unreachable dead code | DCE |
| O108 | Eliminate transitively dead code | ADCE |
| O109 | Eliminate dead stores | DSE |
| O110 | Canonicalise expressions (strength reduction, reassociation) | InstCombine |
| O111 | Brace expression text for bytecode compilation (paired with W100) | Performance hint |
| O112 | Eliminate constant-condition compound statements | Structure elimination |
| O113 | Strength-reduce expressions (x**2 → x*x, x%8 → x&7) |
Strength reduction |
| O114 | Recognise incr idiom (set x [expr {$x + N}] → incr x N) |
Idiom recognition |
| O115 | Remove redundant nested [expr {...}] in expression context |
Simplification |
| O116 | Fold constant [list a b c] to literal |
Constant folding |
| O117 | Simplify [string length $s] == 0 → $s eq "" |
Peephole |
| O118 | Fold constant [lindex {a b c} 1] to element |
Constant folding |
| O119 | Pack consecutive set literals into lassign/foreach |
Statement packing |
| O120 | Prefer eq/ne over ==/!= for string comparisons |
Type-aware rewrite |
| O121 | Rewrite self-recursive tail calls to tailcall |
Tail-call optimisation |
| O122 | Convert fully tail-recursive proc to iterative while loop |
Recursion elimination |
| O123 | Detect non-tail recursion eligible for accumulator introduction | Recursion analysis |
| O124 | Comment out unused procs in iRules (not called from any event) | Dead proc elimination |
| O125 | Sink assignment into deepest decision block that uses it | Code sinking |
- Python 3.10+
- uv (Python package manager)
- Node.js 20+ with npm
- VS Code 1.93+
# Clone and enter the repo
git clone <repo-url>
cd tcl-lsp
# Run tests
make test
# Build the .vsix
make vsix
# Install in VS Code
code --install-extension tcl-lsp-vscode-0.1.0.vsixRun make help to see all targets:
| Target | Description |
|---|---|
make test-pr |
Full CI gate — lint + Python tests + extension tests + smoke tests |
make vsix |
Build the .vsix (tests must pass first) |
make install |
Build and install the .vsix into VS Code |
make package-vsix |
Package VSIX (skip lint/test, for CI) |
make test |
Run all tests (Python + VS Code extension) |
make test-py |
Run the Python test suite only |
make test-ext |
Run VS Code extension integration tests |
make lint |
Run all lint and style checks |
make lint-py |
Lint Python code with Ruff |
make typecheck-py |
Type-check Python code with ty |
make lint-ts |
Lint/format-check TypeScript extension code |
make format-py |
Format and auto-fix Python code with Ruff |
make npm-env |
Install/update npm dependencies |
make compile |
Compile the TypeScript extension |
make zipapps |
Build all zipapps (Tcl, CLI, GUI, GUI-CDN, LSP, AI, MCP, WASM) |
make zipapp-tcl |
Build the unified Tcl tools zipapp |
make zipapp-cli |
Build the CLI compiler explorer zipapp |
make zipapp-gui |
Build the standalone GUI zipapp (bundles Pyodide) |
make zipapp-gui-cdn |
Build the CDN GUI zipapp (loads Pyodide from CDN) |
make zipapp-lsp |
Build the LSP server zipapp |
make zipapp-ai |
Build the AI analysis zipapp |
make zipapp-mcp |
Build the MCP server zipapp |
make zipapp-wasm |
Build the WASM compiler zipapp |
make claude-skills |
Build Claude Code skills release zip |
make jetbrains |
Build the JetBrains plugin (.zip) |
make sublime |
Build the Sublime Text package (.sublime-package) |
make zed |
Build the Zed extension (.tar.gz WASM artifact) |
make screenshot |
Alias of make screenshots |
make screenshots |
Capture extension screenshots and build demo GIF (macOS) |
make release |
Build all release artifacts (parity with tagged CI release jobs) |
make release-tag |
Bump version, annotated-tag, and push (V=x.y.z) |
make clean |
Remove build artifacts |
make distclean |
Remove build artifacts and node_modules |
Artifact version strings are derived from git describe (with v stripped).
If Git metadata is unavailable, builds fall back to dev (and semver-constrained
manifest fields use 0.0.0-dev).
make vsix is the main entry point. It runs the test suite first and will
not package a .vsix if any test fails. Packaging uses an isolated staging
directory under build/vsix-stage/, and the output file lands under
build/ as tcl-lsp-<version>.vsix.
On macOS, make screenshots prefers a small Swift window-probe helper when
swiftc is available, so captures use deterministic
screencapture -o -l <window-id>. If Swift is unavailable, it falls back to
AppleScript-based probing.
By default, make screenshots auto-installs missing screenshot tools with
Homebrew (pngquant, oxipng, gifsicle, and imagemagick when needed).
To disable auto-install, run:
TCL_LSP_SCREENSHOT_AUTO_BREW=0 make screenshots.
By default, screenshot runs are isolated:
- downloaded VS Code
stablevia@vscode/test-electron - isolated user data (
~/.tcl-lsp-screenshots/user-data) - isolated extensions dir (
~/.tcl-lsp-screenshots/extensions) - allowlisted external extensions only (
github.copilot-chat)
Useful overrides:
- Reuse your normal VS Code user data:
TCL_LSP_SCREENSHOT_REUSE_CODE_USER_DATA=1 make screenshots - Use local app bundle instead of downloaded VS Code:
TCL_LSP_SCREENSHOT_USE_SYSTEM_VSCODE=1 TCL_LSP_SCREENSHOT_FORCE_DOWNLOADED_VSCODE=0 make screenshots - Change allowed external extensions (comma-separated extension IDs):
TCL_LSP_SCREENSHOT_ALLOWED_EXTENSIONS=github.copilot-chat make screenshots
- Production dependency audits are enforced with
npm audit --omit=dev. - Dev-only audit findings are accepted and do not block releases in this repository.
tcl-lsp/
Makefile Build system
pyproject.toml Python project metadata (hatchling)
lsp/ Python LSP server
__main__.py Entry point (python -m server)
server.py pygls server, handler wiring
async_diagnostics.py Background diagnostic scheduler (tiered publishing)
analysis/
analyser.py Single-pass semantic analyser
checks.py Best-practice and security checks (W-series)
irules_checks.py iRules-specific best-practice checks (IRULE-series)
semantic_model.py Data model (scopes, procs, diagnostics)
semantic_graph.py Call/symbol/data-flow graph queries
bigip/
parser.py BIG-IP configuration file parser
model.py BIG-IP configuration data model
rule_extract.py iRule extraction from BIG-IP configs
validator.py Configuration validation
diagnostics.py BIG-IP-specific diagnostics
commands/
registry/
models.py CommandSpec dataclass (arity, roles, dialect flags)
command_registry.py CommandRegistry class (query methods)
runtime.py Registry runtime (dialects, roles, body/expr index helpers)
signatures.py Argument signature helpers
namespace_registry.py Namespace registry (event/command metadata facade)
namespace_data.py Canonical event/command data tables
namespace_models.py Namespace model dataclasses
operators.py Operator definitions and hover data
taint_hints.py Per-command taint source/sink hints
type_hints.py Per-command return type hints
tcl/ One file per Tcl command (@register decorator)
irules/ F5 iRules command definitions
iapps/ F5 iApps template command definitions
tk/ Tk widget command definitions
tcllib/ tcllib package command definitions
stdlib/ Tcl standard library command definitions
common/
dialect.py Active dialect state
naming.py Name normalisation helpers
ranges.py Range/position utilities
packages/
resolver.py Tcl package require resolution
compiler/
lowering.py Tcl source -> IR lowering
ir.py IR node definitions
cfg.py Control flow graph construction
ssa.py Static single assignment form
core_analyses.py SCCP, liveness, type inference, dead store detection
compilation_unit.py Compile pipeline orchestration and caching
compiler_checks.py IR-to-diagnostics (arity, subcommands)
optimiser.py Source rewrite passes (O100–O125)
gvn.py GVN/CSE/PRE/LICM redundant computation detection (O105–O106)
interprocedural.py Call graph, function purity/side-effect summaries
taint.py Data taint analysis (T100–T106, IRULE3xxx)
shimmer.py Tcl object representation analysis (S100–S102)
irules_flow.py iRules control-flow checks (IRULE1xxx/4004/5xxx)
codegen.py Tcl VM bytecode assembly backend
static_loops.py Conservative static evaluation for for-loops
tcl_expr_eval.py Tcl expression evaluator (constant folding)
expr_ast.py Expression AST parser
expr_types.py Expression type inference
effects.py Command side-effect classification
connection_scope.py iRules connection-scope variable tracking
types.py Type lattice definitions
token_helpers.py Shared token-stream utilities
eval_helpers.py Evaluation helper constants
diagram/
extract.py iRule event-flow diagram extraction
features/
code_actions.py Quick-fix code actions
completion.py Completions
definition.py Go to definition
diagnostics.py Diagnostic aggregation (internal -> LSP)
formatting.py LSP formatting handlers
hover.py Hover information
inlay_hints.py Inlay hint provider (inferred types, format strings)
references.py Find references
rename.py Rename symbol
call_hierarchy.py Call hierarchy (incoming/outgoing calls)
document_symbols.py Document symbol hierarchy
document_links.py Document link provider
folding.py Folding range provider
selection_range.py Selection range provider
signature_help.py Signature help provider
workspace_symbols.py Workspace symbol search
semantic_tokens.py Semantic token provider
snippet_templates.py Tcl/iRules snippet templates
symbol_resolution.py Shared word/variable/scope resolution helpers
parsing/
lexer.py Tcl lexer with position tracking
tokens.py Token and position types
command_segmenter.py Command segmentation from token stream
recovery.py Centralised error recovery via virtual tokens
expr_lexer.py Expression sub-lexer
expr_parser.py Expression sub-parser
substitution.py Tcl backslash substitution helpers
tk/
detection.py Tk widget auto-detection
diagnostics.py Tk-specific diagnostics
extract.py Tk widget hierarchy extraction
workspace/
document_state.py Per-file analysis cache (dialect-gated profile scanning)
workspace_index.py Cross-file proc index (O(1) tail lookup, usage caching)
scanner.py Background workspace file scanner
xc/
translator.py iRules-to-XC migration translator
mapping.py iRules → XC command mapping table
xc_model.py XC output data model
terraform.py Terraform HCL generation
json_api.py JSON API for XC translation
diagnostics.py Migration diagnostics
explorer/ Compiler explorer (CLI + web GUI)
cli.py CLI interface
pipeline.py Compilation pipeline wrapper
serialise.py Output serialisation (IR, CFG, SSA, optimiser)
formatters.py Display formatters
static/ Web GUI assets (Pyodide)
ai/ AI integrations
claude/
skills/ Claude Code skills (20 CLI commands)
mcp/
tcl_mcp_server.py MCP server for Claude Desktop integration
prompts/ System prompts for Tcl/iRules/Tk
shared/ Shared diagnostics manifest and utilities
tests/ pytest test suite
editors/
vscode/ VS Code extension client (.vsix)
package.json Extension manifest
tsconfig.json TypeScript config
src/extension.ts Extension entry point
language-configuration.json
syntaxes/tcl.tmLanguage.json
neovim/ Neovim LSP config (Lua)
zed/ Zed extension (TOML + Rust WASM)
emacs/ Emacs eglot / lsp-mode config
helix/ Helix languages.toml config
sublime-text/ Sublime Text package (syntax, LSP, snippets)
jetbrains/ JetBrains plugin (Gradle/Kotlin)
See CONTRIBUTING.md for coding-style and packaging rules.
The server communicates over stdio. To launch it directly:
uv run python -m serverThis is useful for debugging or for use with any LSP client.
See editors/ for per-editor setup instructions.
# Via make (sets up the venv automatically)
make test
# Or directly with uv
uv run --extra dev pytest tests/ -v
# Run a specific test file
uv run --extra dev pytest tests/test_checks.py -v
# Run tests matching a pattern
uv run --extra dev pytest tests/ -k "unbraced_expr"
# Lint Python code
make lint-py
# Type-check Python code
make typecheck-py
# Auto-fix and format Python code
make format-pyUse tcl_compiler_explorer.py to inspect how source is lowered and optimised:
# Full compiler + optimiser exploration
uv run python tcl_compiler_explorer.py samples/for_screenshots/22-optimiser-before.tcl
# Focus on optimiser rewrites only
uv run python tcl_compiler_explorer.py samples/for_screenshots/22-optimiser-before.tcl --focus optimiser
# Inline source with explicit optimised output
uv run python tcl_compiler_explorer.py --source 'set a 1; set b [expr {$a + 2}]' --show-optimised-sourceThe explorer renders:
- lowered IR and per-procedure bodies
- CFG pre-SSA and post-SSA (with use/def and inferred constants)
- interprocedural summaries
- optimiser rewrites
- source callouts with caret markers and
+-->arrows for salient spans
# Install npm deps
make npm-env
# Watch mode (recompiles on save)
cd editors/vscode && npm run watchTo test the extension in VS Code, open editors/vscode/ in VS Code and press
F5 to launch the Extension Development Host.
During development you can point the extension at your working copy instead
of the bundled server. Set tclLsp.serverPath in your VS Code settings:
{
"tclLsp.serverPath": "/path/to/tcl-lsp"
}The extension will use uv run from that directory, so changes to the Python
source take effect on the next editor reload.
- Add a check function to the appropriate submodule in
core/analysis/checks/(e.g._security.py,_style.py,_domain.py,_syntax.py) following the existing pattern -- each check receives the command name, argument texts, argument tokens, all tokens, and the source string. - Register it in the
ALL_CHECKSlist incore/analysis/checks/_orchestrator.py. - If the check can be auto-fixed, include a
CodeFixin the diagnostic'sfixestuple. - Add tests to
tests/test_checks.py. - Run
make testto verify.
- Add the field to
FormatterConfigincore/formatting/config.py. - Handle it in
core/formatting/engine.py. - Add
to_dict/from_dictsupport if the field uses a non-primitive type. - Add tests to
tests/test_formatter.py. - Keep consumers on core imports (
core/formatting/*) and delete legacy import paths in the same change. - Run
tests/test_core_lift_consumers.pyto verify no downstream consumer is importing shim modules.
Server/runtime settings are available through the tclLsp.* namespace.
| Setting | Default | Description |
|---|---|---|
dialect |
tcl8.6 |
Command/signature profile (tcl8.4, tcl8.5, tcl8.6, tcl9.0, f5-irules, f5-iapps, f5-tmsh, f5-bigip, synopsys-eda-tcl, cadence-eda-tcl, xilinx-eda-tcl, intel-quartus-eda-tcl, mentor-eda-tcl, expect) |
extraCommands |
[] |
Extra command names treated as known varargs commands |
libraryPaths |
[] |
Additional directories to scan for Tcl packages and libraries |
Formatter options are available through tclLsp.formatting.* (defaults based
on the F5 iRules Style Guide):
| Setting | Default | Description |
|---|---|---|
indentSize |
4 |
Spaces per indent level |
indentStyle |
spaces |
spaces or tabs |
continuationIndent |
4 |
Extra indentation for continuation lines |
braceStyle |
k_and_r |
k_and_r |
spaceBetweenBraces |
true |
Space between consecutive braces (} { vs }{) |
enforceBracedVariables |
false |
Rewrite $var as ${var} |
enforceBracedExpr |
false |
Require braced expressions |
maxLineLength |
120 |
Hard line length limit |
goalLineLength |
100 |
Soft target for line length |
expandSingleLineBodies |
false |
Force multi-line bodies |
minBodyCommandsForExpansion |
2 |
Minimum commands in body before expansion |
spaceAfterCommentHash |
true |
Space between # and comment text |
trimTrailingWhitespace |
true |
Remove trailing whitespace |
alignCommentsToCode |
true |
Align inline comments to a consistent column |
replaceSemicolonsWithNewlines |
true |
Convert ; to newlines |
blankLinesBetweenProcs |
1 |
Blank lines separating proc definitions |
blankLinesBetweenBlocks |
1 |
Blank lines between control flow blocks |
maxConsecutiveBlankLines |
2 |
Maximum consecutive blank lines allowed |
lineEnding |
lf |
Line ending style (lf, crlf, cr) |
ensureFinalNewline |
true |
Ensure file ends with a newline |
| Setting | Default | Description |
|---|---|---|
shimmer.enabled |
true |
Enable shimmer detection (S-series diagnostics) |
Optimiser toggles are available through tclLsp.optimiser.*:
| Setting | Default | Description |
|---|---|---|
enabled |
true |
Enable optimiser suggestions as diagnostics |
O100 |
true |
Enable constant propagation rewrites |
O101 |
true |
Enable constant expression folding rewrites |
O102 |
true |
Enable [expr {...}] command substitution folding rewrites |
O103 |
true |
Enable static procedure-call folding rewrites |
O104 |
true |
Enable static string-build folding rewrites |
O105 |
true |
Enable constant var-ref propagation and redundant computation detection (GVN/CSE/PRE) |
O106 |
true |
Enable loop-invariant computation hoisting (LICM) |
O107 |
true |
Enable unreachable dead code elimination (DCE) |
O108 |
true |
Enable transitive dead code elimination (ADCE) |
O109 |
true |
Enable dead store elimination (DSE) |
O110 |
true |
Enable expression canonicalisation (InstCombine) |
O111 |
true |
Enable paired performance hints for unbraced expression warnings (W100) |
O112 |
true |
Enable constant-condition structure elimination |
O113 |
true |
Enable strength reduction (x**2 → x*x) |
O114 |
true |
Enable incr idiom recognition |
O115 |
true |
Enable redundant nested expr elimination |
O116 |
true |
Enable constant list folding |
O117 |
true |
Enable string length zero-check simplification |
O118 |
true |
Enable constant lindex folding |
O119 |
true |
Enable multi-set packing (lassign/foreach) |
O120 |
true |
Enable type-aware ==/!= to eq/ne string comparison rewrite |
O121 |
true |
Enable self-recursive tail-call rewriting to tailcall |
O122 |
true |
Enable tail-recursive proc conversion to iterative loop |
O123 |
true |
Enable non-tail recursion accumulator pattern detection |
O124 |
true |
Enable unused iRule proc commenting |
O125 |
true |
Enable code sinking into decision blocks |
All diagnostic codes can be toggled individually via
tclLsp.diagnostics.<CODE>: true/false. The main series are:
| Series | Codes | Area |
|---|---|---|
| E | E100–E999 | Errors |
| W | W100–W299 | General warnings |
| W | W300–W309 | Security warnings |
| S | S100–S102 | Shimmer detection |
| T | T100–T102 | Taint analysis |
| H | H100+ | Hints |
| IRULE | IRULE1001–IRULE5005 | iRules-specific diagnostics |
Settings can be stored in an INI file at
~/.config/tcl-lsp/config.ini (or $XDG_CONFIG_HOME/tcl-lsp/config.ini
if $XDG_CONFIG_HOME is set). This is useful for editor-agnostic defaults
that apply across all workspaces.
Precedence (applied in order — later entries override earlier):
- Built-in defaults
- XDG config file
- Editor settings (VS Code
settings.json, Neovimlspconfig, etc.)
The file uses INI format with section names matching the tclLsp.*
namespace:
[diagnostics]
disabled = W111, T100
[optimiser]
disabled = O109
[shimmer]
enabled = true
[features]
inlayHints = false
[formatting]
indent_size = 2See docs/kcs/kcs-xdg-config.md for the
full reference.
In VS Code, run the command "Tcl: Export Settings to XDG Config"
from the command palette. For other editors, send the
tcl-lsp.exportConfig request via workspace/executeCommand.
Only non-default values are written, keeping the generated config file minimal.
{
"tclLsp.dialect": "f5-irules",
"tclLsp.extraCommands": ["myCompany::command"]
}This project was inspired by:
- Picol by Salvatore Sanfilippo (antirez) -- a minimal Tcl interpreter in C that demonstrates the elegance of the Tcl parsing model
- iRuleScan by Simon Kowallik -- a security scanner for F5 iRules
- tclint-vscode by Noah Moroze -- a Tcl linter with VS Code integration
This project used AI very heavily.
- The core parser, lexer, IR, CFG were largely hand created with input on AI about structure, and lots of AI code review.
- The command registry was seeded by hand then filled out with AI.
- The vscode extension, compiler explorer, editor integrations, CI/CD, build pipelines VM, and compiler to Tcl bytecode were all entirely vibe coded.
- The Claude skills, AI integrations were vibe coded with hand work on the prompts .. they need more of that.
- The vast bulk of tests were AI written, AI ported from sources like Tcl, but all largely directed by me in their creation. If I'd been doing that by hand you'd see 3 tests and they'd all be "make install worked for me, good luck"
- Claude Opus 4.6, Gemini 3.1 Pro and OpenAI GPT-5.3-Codex were all used to review the code, critise it, rewrite and reorganise it.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later).
You are free to use this tool as-is. If you modify the code or incorporate portions of it into another project, the AGPL requires that the complete source of the derivative work is made available under the same license.
Upstream contributions strongly preferred. If you improve or extend this project, please submit your changes back as a pull request rather than maintaining a private fork. See CONTRIBUTING.md for details.

















