tcl-lspA language server for Tcl with multi-editor support.
The server is a native Rust binary ( ContentsStart here — Install · The seven you will use most · Full feature index The language — Dialects, languages, and packages · F5 BIG-IP · Diagnostic and optimiser codes Tools — Compiler explorer · Command registry spec studio · WASM, the bytecode VM, and eBPF · AI tooling · CLI tools · Packaging & environments Tuning it — Suppressing diagnostics · Diagnostic prominence · Multi-file projects · Configuration Contributing — Building and contributing · Screenshots · Licence InstallGrab the artefact for your editor from Releases, or install the VS Code extension from the Marketplace. Nothing needs Python — the server is a self-contained native binary. While the Rust rewrite is on the pre-release channel, install it from the
VS CodeInstall from the
Marketplace,
which serves your platform's package automatically, or sideload the
The extension bundles the native server — nothing else to install. Settings live under Settings > Extensions > Tcl. The seven you will use mostEverything below this section is available too — this is what you will actually touch on a normal day. The full feature index has the rest. 1. Diagnostics that catch real bugsArity errors, unknown subcommands, best-practice violations, and security issues are reported with precise ranges. Diagnostics can be suppressed inline, per-file, per-project, per-editor, or globally — see Suppressing diagnostics.
Child interpreters are modelled too: a command hidden in a safe
interpreter (
A direct call into one of the 11 private, undocumented
Analysis is file-aware where Tcl semantics demand it: in a A proc, class,
Diagnostics see the whole workspace, not just the open file. A call to a
proc defined in a sibling file is recognised as the command it really reaches,
and a call with the wrong number of arguments is reported with the same codes
as a same-file one — go-to-definition and the Problems panel answer from one
shared lookup, so they cannot disagree about whether a name exists. Matching is
by fully-qualified name in Tcl's own resolution order, so a
Where a fact cannot be proven the analyser says nothing rather than guessing: a
The eval family (
Object typing follows commands through
The named-object construction form (
The grammar of a command follows its binding too. A statically
visible
W113 ("proc shadows a built-in") only fires for a genuine core
built-in. A proc named after a command that is gated behind
What the analyser checksSeven families of finding, each code with its own page explaining why the check exists, a triggering example, and the fix:
Full tables: diagnostic codes · optimiser codes · per-code pages. 2. Dialect-aware semantic highlightingVariables, procs, keywords, and strings are classified using SSA-informed type
information, giving richer highlighting than a TextMate grammar alone. The
server provides 44 token types beyond the standard LSP set, including
sub-token highlighting inside strings. Tokens are cached per top-level chunk
so only dirty regions are recomputed after an edit, and the server supports
Token requests are prioritised over deeper analysis: a large or freshly opened file serves an immediate baseline response instead of waiting behind a full analysis pass, then the server pushes a refresh once the fully analysis-enriched tokens (regex-aware retagging, resolved object-method dispatch) are ready — so highlighting never stalls waiting on the analyser, only briefly starts coarser on a cold file.
In addition to standard token types (keyword, function, variable, string, comment, number, operator, parameter, namespace), the server provides domain-specific token types:
Command options are highlighted precisely for commands the registry knows,
including object methods on a tracked handle — the standard A Tk/ttk widget's own instance command resolves the same way, keyed on the
widget path rather than an object handle: Script-body arguments are highlighted as scripts, not opaque strings. The body
of an 3. CompletionsContext-aware completions for commands, subcommands, variables, proc names
(workspace-wide), switch arms, and
Signature helpAs you type arguments, the server shows the expected parameter list with the active parameter highlighted.
4. Hover documentationHovering on a command, proc call, variable, or operator shows its signature,
doc comment, and type information. Multi-line docstrings are supported, and
5. Navigation: definition, references, call hierarchyJump to the definition of a proc or variable — works across files in the workspace.
Find referencesLocate every usage of a proc or variable, including inside nested braced
script bodies such as
References follow Hover and completion read the same registry data. Hovering a bare A A class is found through every use of its name, not only
Call hierarchyInspect incoming callers and outgoing callees for any procedure.
|
| Feature | Note |
|---|---|
| Completions | completions |
| Hover | hover |
| Signature Help | signature-help |
| Go to Definition | definition |
| Find References | references |
| Call Hierarchy | call-hierarchy |
| Document Symbols | document-symbols |
| Workspace Symbols | workspace-symbols |
| Document Highlight and Linked Editing | document-highlight |
| Document Links | document-links |
| Folding | folding |
| Selection Range | selection-range |
| Inlay Hints | inlay-hints |
| Code Lens | code-lens |
| Semantic Tokens | semantic-tokens |
| Command option highlighting | command-option-highlighting |
| Command Info | command-info |
| Type Navigation (TclOO) | type-navigation |
| Special Variable Recognition | special-variables |
Diagnostics and analysis
| Feature | Note |
|---|---|
| Diagnostics | diagnostics |
| Unused Variable Detection | unused-variables |
| Unknown Command Resolution (W123) | unknown-command-resolution |
| Byte-array corruption (S110) | byte-array-corruption |
| Var-escape analysis | var-escape-analysis |
| Optimiser | optimiser |
| Semantic Graphs | semantic-graphs |
| Control-Flow Diagrams | control-flow-diagrams |
| Runtime Validation | runtime-validation |
Changing code
| Feature | Note |
|---|---|
| Refactoring Tools | refactorings |
| Extract into proc | refactor-extract-proc |
| Refactor: Extract Variable | refactor-extract-variable |
| Inline proc | refactor-inline-proc |
| Refactor: Inline Variable | refactor-inline-variable |
| Refactor: if/elseif to switch | refactor-if-to-switch |
| Refactor: switch to dict lookup | refactor-switch-to-dict |
| Refactor: Brace expr | refactor-brace-expr |
| Refactor: Extract to Data-Group | refactor-extract-datagroup |
| Rename | rename |
| Code Actions | code-actions |
| Formatting | formatting |
| Minifier | minifier |
| Unminify Error | unminify-error |
| Modernisation Tools | modernisation-tools |
| Text Transforms | text-transforms |
| Template Snippets | template-snippets |
| Documentation Generation | documentation-generation |
| Test Generation | code-generation |
Dialects, packages, and environments
| Feature | Note |
|---|---|
| Dialect Selection | dialect-selection |
| tcllib package coverage | tcllib-package-coverage |
| Package Management | package-management |
| Package Scaffolding | package-scaffolding |
| tcl pkg | tcl-pkg |
| tcl venv | tcl-venv |
Compiler, VM, and tooling
| Feature | Note |
|---|---|
| Compiler Explorer | compiler-explorer |
| Command Registry Spec Studio | spec-studio |
| Compilation Tools | compilation-tools |
| Tcl Debugger | debugger |
| BPF-Tcl low-level packet language | bpf-tcl |
| Unified Tcl Verb CLI | tcl-verb-cli |
| Tk Preview | tk-preview |
| Extension Settings and Server Control | extension-settings |
AI tooling
| Feature | Note |
|---|---|
| @tcl Chat Participant | ai-chat-tcl |
| @tk Chat Participant | ai-chat-tk |
| AI Help | ai-help |
| Chat Slash Commands | chat-slash-commands |
| Claude Code Skills | claude-code-skills |
| MCP Server | mcp-server |
F5-specific features — the BIG-IP object model, f5 query, the report
generator, iRules analysis and testing, iApps/APL, XC translation — are
indexed in README-f5.md.
The whole set is also browsable in-editor and from the CLI: tcl help <feature>, the MCP help tool, and the VS Code /help chat command all read
these same notes.
Dialects, languages, and packages
Every supported dialect
Eighteen dialect profiles, each gating which commands exist, which are
deprecated, and which options and subcommands are valid. The list below
mirrors the profile catalog (DialectProfile) in rust/tcl-dialect, the
single source of truth — its display_name is the second column.
| Dialect | Language / tooling it models |
|---|---|
tcl8.4 |
Tcl 8.4 |
tcl8.5 |
Tcl 8.5 |
tcl8.6 |
Tcl 8.6 (the default) |
tcl9.0 |
Tcl 9.0 |
tcl9.1 |
Tcl 9.1 |
expect |
Expect |
bpf |
BPF-Tcl, the eBPF packet-matching dialect |
spectcl |
SpecTcl command packs (.tclspec) |
f5-irules |
F5 iRules (embedded Tcl 8.4.6) — see README-f5.md |
f5-iapps |
F5 iApps — iApp templates and implementation scripts |
f5-bigip |
F5 BIG-IP bigip.conf / .scf objects |
f5-tmsh |
F5 tmsh scripts |
cadence-eda-tcl |
Cadence EDA Tcl |
intel-quartus-eda-tcl |
Intel Quartus EDA Tcl |
mentor-eda-tcl |
Mentor EDA Tcl (ModelSim/Questa) |
microchip-libero-eda-tcl |
Microchip Libero EDA Tcl |
synopsys-eda-tcl |
Synopsys EDA Tcl (incl. the SDC constraint base) |
xilinx-eda-tcl |
Xilinx EDA Tcl (AMD/Xilinx Vivado) |
Pick one per file with a # tcl-dialect: comment, per project in
configuration, or let detection choose — see
Automatic dialect detection below.
Every package in the registry
Commands from these 69 packages are modelled with hover docs,
completion, arity checking, argument roles, and side-effect classification.
They activate when their package require appears (or ambiently, when a
dialect ships them):
argparse, base32::core, base64, bibtex, cksum, cmdline, comm, control, cookiejar, crc16, crc32, csv, debug, defer, dns, f5-irules-cmds, fileutil, generator, hook, html, http, inifile, ip, Itcl, json, lambda, logger, math, math::constants, math::statistics, md4, md5, md5crypt, mime, msgcat, namespacex, ooutil, opt, otp, platform, platform::shell, processman, rc4, report, safe, sha1, sha2, smtp, snit, soundex, stooop, stringprep, struct::list, struct::queue, struct::set, struct::stack, sum, tcl::chan::halfpipe, tcl::idna, tcltest, textutil, ticklecharts, tie, Tk, unicode, uri, uuid, websocket, yaml
That includes Tk in full, Itcl, and the whole tcltest surface with
per-version availability (test -errorCode only from tcltest 2.5, bytestring
gone under Tcl 9.0). Most of the rest is tcllib — see
tcllib package coverage
for the module-by-module state, and
kcs-howto-add-command-registry-package.md
to add another.
Third-party and in-house commands that are not in the registry can be declared with stub annotations.
Dialect profiles
Switch between Tcl 8.4/8.5/8.6/9.0/9.1, F5 iRules, F5 iApps, F5 tmsh, and EDA
tooling profiles. Tk, tcllib, and stdlib commands activate automatically when their
package require appears — including the full tcltest surface (test,
configure, and the convenience commands) with per-version awareness, so
test -errorCode is offered only for tcltest 2.5+ and bytestring disappears
under Tcl 9.0. 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. The configured
BIG-IP target also gates subcommands and their enumerated modes: for example,
SSL::c3d cert_lifespan, SSL::c3d cert_start_date, and persist mcp are
offered only for BIG-IP 21.1+.
# 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 ::tcl:: namespace itself is a Tcl 8.5+ addition — plain tcl8.4 and F5
iRules (a real embedded Tcl 8.4.6) have no such namespace at all, so its
contents are gated to their real introduction release:
# With dialect = tcl8.4 (or f5-irules):
::tcl::mathop::+ 1 2 ;# W002: disabled in active dialect (::tcl:: is 8.5+)
tcl::build-info version ;# W002: disabled in active dialect (available in: tcl9.0, tcl9.1)
tcl::tm::path add /some/dir ;# W002: disabled in active dialect (available in: tcl8.5, tcl8.6, tcl9.0, tcl9.1)
Individual tcl::mathop operators can be gated even more precisely than the
namespace itself: lt/le/gt/ge (TIP 461) need Tcl 9.0, one release
newer than the ::tcl:: namespace's own 8.5 baseline:
# With dialect = tcl8.6:
::tcl::mathop::lt 1 2 ;# W002: disabled in active dialect (available in: tcl9.0, tcl9.1)
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.
Version-aware diagnostics reach every gateable level of a call, not just
the command: a subcommand, a second-level operation of a two-level
ensemble (info object class), an option, and a literal argument value —
each independently reports "not introduced yet" (W135/W136),
"deprecated" (W144), or "removed" (W139) against the resolved version
floor. A package require Foo A-B range whose upper bound reaches
past a retirement is caught too: the floor alone can be satisfied while
part of the accepted range is not, so W139 fires with a hedged "not
available in every version satisfying requirement A-B" message rather
than staying silent.
Automatic dialect detection
The dialect is selected automatically using the following priority chain (highest to lowest):
Editor language ID -- opening a file as
tcl-irule,tcl84, etc. selects the matching dialect immediately. (The version-pinned VS Code language ids are undotted --tcl84,tcl85,tcl86,tcl90,tcl91-- because VS Code cannot carry aconfigurationDefaultsoverride for a language id containing a.. The dialect names below keep their dots, and the server still accepts the dottedtcl8.4-style id other editors send.)File extension -- each profile in the catalog owns its extensions:
.irul/.irule/.irules→f5-irules,.iapp/.iappimpl/.impl→f5-iapps,.tmsh→f5-tmsh,.scf→f5-bigip,.exp/.expect→expect,.tclspec→spectcl,.globals→cadence-eda-tcl,.qsf/.qpf/.qip→intel-quartus-eda-tcl,.do→mentor-eda-tcl,.sdc/.upf→synopsys-eda-tcl,.xdc→xilinx-eda-tcl. A SpecTcl pack can route further extensions to a dialect with afile_extensionrow, so a private library's own suffix opens in the dialect it is written for.Comment directive -- a
# tcl-dialect: <dialect>comment in the first 5 lines of a file pins the dialect for that file:# tcl-dialect: tcl8.4 set x 1Shebang --
#!/usr/bin/env tclsh8.5selectstcl8.5;#!/usr/bin/expectselectsexpect.User setting -- the
tclLsp.dialectconfiguration value acts as the default for files that have no per-file hint.Hardcoded fallback --
tcl8.6when nothing else matches.
Per-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.
| 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) |
tcl9.1 |
Tcl 9.1 core commands (superset of 9.0; adds the unicode and timer ensembles and subst's positive -backslashes/-commands/-variables options) |
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 / .scf) commands |
f5-tmsh |
F5 tmsh scripts: the tmsh:: command surface on a Tcl 8.5 base |
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) |
microchip-libero-eda-tcl |
Microchip Libero SoC EDA commands |
expect |
Expect: spawn, expect, send, interact and related commands for automating interactive programs |
bpf |
BPF-Tcl: the eBPF packet-matching dialect |
spectcl |
SpecTcl command packs (.tclspec): the declarations that teach the registry a private library |
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. The tcllib coverage spans the cryptography/hash
(md4, ripemd, crc*, aes/blowfish/des, …), encoding (base32,
ascii85, uuencode, yencode), maths (math, math::fuzzy,
math::roman), data/utility (inifile, units, counter, tie,
lambda), web/protocol/client (asn, ncgi, imap4, ldap, ftp,
pop3, irc, rest, SASL, websocket, …), format (png, jpeg,
tiff, gpx, mapproj, nmea), and ensemble (generator, debug,
hook) package families — see
the tcllib coverage note.
Dialect command stubs
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-end
Multiple 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.
Command alias resolution
When interp alias {} name {} target ?args? creates a command alias in the
current interpreter, the LSP automatically inherits the target command's
argument semantics. This means expression arguments, body arguments, variable
names, and patterns are all correctly analysed through the alias:
interp alias {} = {} expr
proc calculate {x y} {
set result [= {$x + $y}] ;# $x and $y recognised as reads — no W214
return $result
}
Alias information is also used by LSP features: hover shows the target command's documentation, completion offers aliases as candidates, go-to-definition follows aliases to the target proc, and signature help shows the target's parameter hints.
See KCS: Command alias resolution for details.
Proc argument trait inference
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.
F5 BIG-IP
F5 support is a first-class part of tcl-lsp, and large enough to have its own document: README-f5.md.
It covers the f5-irules, f5-iapps, f5-bigip, and f5-tmsh dialects; the
BIG-IP configuration model and iRule extraction; the f5 CLI (query,
cleanup, grep, irule, report); the jq-shaped
query DSL and its Python (f5q) bindings;
the standalone HTML report generator; iRules-to-XC translation; and the iRule
Event Orchestrator test framework with fakeCMP multi-TMM simulation.
Compiler explorer
Compiler explorer
An interactive webview panel (Ctrl+Alt+E / Cmd+Alt+E) that visualises the
compiler's intermediate representation, control-flow graph, SSA form,
optimiser output, Tcl bytecode, and WebAssembly disassembly for the active
editor. The WASM tab renders each instruction with its originating Tcl
source range (click an instruction to place the source cursor inside the
expression, substituted command, or post-; sub-command it compiled from),
resolved call targets (click call 42 ; ::greet to jump to both the
callee's disassembly and its definition), resolved branch targets (click
br 0 ; loop_header foreach to jump to the matching loop open), a
labelled block / loop / if for each Tcl construct (foreach,
while, for, if, catch body, switch arm), a source-line comment
above every instruction group, and orthogonal control-flow arrows in the
left gutter.
The Interproc tab opens with a unit scope card: which registry-declared
boundaries the file crosses (package provide, source, namespace export,
…), whether the analysis had a cross-file view of the workspace, the
per-argument verdict behind every interprocedural constant fold, and the
param constants each procedure was actually analysed under. That last line
is the direct answer to "why did this condition fold?" — and, by its absence,
to "why didn't it?". It is the first place to look when a constant fold — or
its absence — is a surprise; the same data is the unitScope view in the
tcl explore CLI and TUI.
The IR, CFG, SSA, bytecode, and WASM tabs each carry an optimiser lens
(off / on / diff). The diff mode compares the relevant node — IR
statement, CFG block, or bytecode instruction — rather than raw text, so
byte offsets, source ranges, sequence indices, and tree-connector glyphs
that merely shift when the optimiser adds or removes a node are ignored.
A single rewrite then shows as a single localised change instead of every
following line being flagged. The tcl-explorer CLI and TUI render the
same offset-free diff via --opt diff.
┌─────────────────────────────────────────────────┐
│ 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] │
│ ... │
└─────────────────────────────────────────────────┘




Compiler explorer (CLI)
Console tool for inspecting the compiler pipeline: IR, CFG, SSA, optimiser rewrites, shimmer warnings, taint analysis, and bytecode.
# Full exploration of a Tcl file
tcl explore script.tcl
# Focus on optimiser rewrites only
tcl explore script.tcl --show opt
# Inline source with optimised output
tcl explore --source 'set a 1; set b [expr {$a + 2}]' --show-optimised-source
# Show only IR and CFG
tcl explore script.tcl --show ir,cfg
# iRules dialect with flow analysis
tcl explore irule.tcl --dialect bigip --show irules
# Serve the embedded web GUI
tcl explore --serve
Available views: ir, cfg, ssa, interproc, types, opt, gvn,
shimmer, taint, irules, callouts, asm, wasm. Groups: all,
compiler, optimiser.
Compiler explorer (web GUI)
The embedded web UI for the compiler explorer is served by the native tcl
binary via tcl explore --serve. The same Rust → WebAssembly module
(make explorer-wasm) is bundled into the VS Code and JetBrains panels, which
compile in the webview itself — offline, with no LSP roundtrip.
# Serve the embedded web GUI
tcl explore --serve
# Choose a port
tcl explore --serve --port 8080
Command registry spec studio
A web page for exploring the command registry: browse every command tcl-lsp
knows for a chosen dialect, edit any field of its CommandSpec, and render the
result back out as a drop-in registry .rs module (copyright banner included)
or a Tcl dialect stub. Live at
bitwisecook.github.io/tcl-lsp/spec-studio,
or build it locally with make spec-studio-wasm.
The form is generated from a schema the registry itself reports, so it covers
every field of CommandSpec and SubCommand — a field added to the registry
appears in the studio without a front-end change, and a drift test fails if
one is ever missed.
Import a package takes a package's own .tcl files, runs the real
analyser over them, and infers a starting spec for each proc it finds:
arity from the parameter list, argument roles and traits from how each
parameter is used in the body (evaluated as a script, upvar'd and
written, iterated as a list), hover text from the doc comment, and a
package require gate from package provide. Every guess is listed with
the evidence behind it.
Import several releases instead of one — a .zip per release, uploaded or
fetched from GitHub — and it derives each command's version range from what
the releases actually witness: introduced_version from the first release the
command appears in, retired_version from the first it is gone from, with the
reasoning shown beside every bound. The same derivation backs tcl spec import on the command line.
Browse the list to pick a command, or type a name you already know and press Load (Enter works too, and the box suggests matching names as you type).
The Pack DSL tab holds a SpecTcl pack's
.tclspec source directly as its own authoritative document — edit the
form and the text follows, edit the text and the form follows. It is a
Monaco editor driven by the actual Tcl language server, compiled to
WebAssembly and running in a Web Worker in your browser: the same server
binary your editor talks to, so the semantic colouring, hovers,
completions, diagnostics, and formatting are the ones your editor shows,
not an approximation of them. The Test tab's Tcl sample gets the same
editor, opened under whichever dialect you have selected. If the server
cannot start the page says so and falls back to a plain text editor with
the pack's own highlighting.
It works on a phone as well as a desktop: the toolbar unwraps to full-width controls, the tab strip scrolls sideways, and touch targets meet the 44px minimum. On a narrow screen the command list moves below the editor, which is why loading by name matters there.
The registry, the compiler's analyser, both renderers, and the language server
are all compiled to WebAssembly and served from the page's own directory. Its
content security policy lets the page reach exactly two outside hosts,
api.github.com and codeload.github.com, and one clearly-labelled opt-in
panel is the only thing that can use them — the release fetcher above, which
acts only when you fill it in and press the button, and which has an offline
.zip upload path that does the same job. Nothing else you type or import can
leave your browser. Copy the output, download it, or open a pre-filled GitHub
issue proposing the spec.




Compiling Tcl: WASM, the bytecode VM, and eBPF
The compiler front end is shared by the language server and by three back ends.
Tcl-to-WASM compiler
Compile Tcl scripts to WebAssembly (WAT text or binary WASM format) with the
tcl compwasm verb.
# Compile to WASM binary (+ optional WAT sidecar)
tcl compwasm script.tcl -o out.wasm --wat-output out.wat
# Compile inline source
tcl compwasm --source 'set x [expr {1+2}]' -o out.wasm
Tcl VM
A 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.
The VM ships as the native tclvm binary.
# Execute a script (trailing args become the script's argv)
tclvm script.tcl arg1 arg2
# Interactive REPL
tclvm
# Inline evaluation
tclvm -c 'puts [expr {6 * 7}]'
# Show bytecode disassembly without executing
tcl dis script.tcl
eBPF (BPF-Tcl)
A low-level packet-matching language that compiles Tcl-shaped source to eBPF — see kcs-feature-bpf-tcl.md.
Tcl debugger
An interactive debugger that can single-step through Tcl scripts with
breakpoints, variable inspection, and call stack visualisation, driven by the
project's own bytecode VM. It ships as the native tcl-debug binary.
# Debug a script (interactive CLI)
tcl-debug script.tcl
# Speak the Debug Adapter Protocol over stdio (for an editor)
tcl-debug --dap
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.
AI tooling
Chat participants
Three chat participants integrate with GitHub Copilot to provide domain-specific AI assistance backed by the LSP's static analysis.
@tcl — Tcl assistant
| 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
@tk — Tk GUI assistant
| 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
Claude Code skills
Twenty purpose-built skills for Claude Code (CLI) that combine LSP static
analysis with AI reasoning. The skills are native — each calls the
tcl-mcp MCP server's tools, 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 |
spec-author |
Build command specs for a private Tcl library from compiler-inferred evidence |
# 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.tcl
MCP server (Claude Desktop / AI agents)
A Model Context Protocol server that exposes tcl-lsp analysis to any MCP-compatible client (Claude Code, Claude Desktop, Codex, custom agents).
The server is the native Rust tcl-mcp binary — a single self-contained
executable that calls the Rust analysis crates directly (no Python, no PyO3).
It hosts the full tool surface (46 tools: analysis, LSP features, refactors,
diagnostics, docstrings, iRule/BIG-IP tools, XC translation, Tk layout, test
generation, …). Build it with make rust-mcp.
Install / register. The installer fetches the prebuilt native binary for
your platform from the GitHub release (tcl-mcp-<triple>), verifies its
checksum, detects supported AI harnesses, and asks separately whether to
register each one. If the current project contains that harness's files, the
installer offers project or user scope; otherwise it uses user scope. Claude
Code, Codex, Gemini CLI, GitHub Copilot CLI, OpenCode, Hermes, Goose, and
Bobbit are recognised (Bobbit supports project scope only):
./scripts/install/install.sh # fetches + registers the native binary
TCL_LSP_MCP_BIN=/path/to/tcl-mcp ./scripts/install/install.sh— register a local build instead of downloading.
Working inside this repo, compatible harnesses auto-discover the server via
the committed .mcp.json, which launches
scripts/tcl-mcp: it prefers a local build
(make rust-mcp), else a cached binary, else fetches the release asset for the
host platform, else builds from source. Register globally without the installer
with make rust-mcp && claude mcp add tcl-lsp -- "$(pwd)/target/release/tcl-mcp".
| Tool | Description |
|---|---|
analyze |
Full analysis: diagnostics, symbols, events, and metadata |
validate |
Categorised validation report |
review |
Security-focused diagnostic report |
find-legacy |
Detect legacy patterns eligible 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 (native binary)
{
"mcpServers": {
"tcl-lsp": {
"command": "/absolute/path/to/target/release/tcl-mcp"
}
}
}
The iRules-specific assistant (@irule) and the F5 skills are documented in README-f5.md.
CLI tools
All CLI tools are distributed as native binaries (tcl, f5-query) — no
runtime required.
Unified Tcl tool (tcl)
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 datacommand-info— look up command registry metadatafind-legacy— detect legacy modernisation patterns (detection only)dis— 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 indexpkg— package management:init,add,remove,install,list,tree,verify,info,search,update,sync,outdated,why,vendor,runvenv— virtual environments:create,delete,info,activate,deactivate,list,update,runspec— author SpecTcl (.tclspec) command packs:importderivesintroduced_version/retired_versionranges for a package's commands from several labelled release snapshots
# Optimise everything under src/ into one output script
tcl opt src/ -o build/optimised.tcl
# Run diagnostics across a directory and a Tcl package
tcl diag src/ mypkg --package-path ./vendor/tcl
# Run lint diagnostics (same checks as `diag`)
tcl lint src/ mypkg --package-path ./vendor/tcl
# Validate syntax/error diagnostics
tcl validate src/
# Validate as JSON
tcl validate src/ --json
# Format source text
tcl format script.tcl -o formatted.tcl
# Minify source (strip comments, collapse whitespace, join commands;
# semantics-preserving — never renames symbols or adds variables)
tcl minify script.tcl -o minified.tcl
# Compact minify (also renames proc-local variables; proc names and
# global variables are renamed only with --isolated, since they are
# observable public identities — see docs/kcs/features/kcs-feature-minifier.md)
tcl minify --compact script.tcl -o minified.tcl --symbol-map map.txt
# Aggressive minify (optimise + static substring folding via SCCP + name
# compaction + alias preambles; NOT frame-transparent — it introduces
# helper variables visible to `info vars` and traces)
tcl minify --aggressive script.tcl -o minified.tcl --symbol-map map.txt
# Symbol/graph/find-legacy analysis verbs
tcl symbols script.tcl --json
tcl diagram script.tcl --json
tcl callgraph script.tcl --json
tcl symbolgraph script.tcl --json
tcl dataflow script.tcl --json
tcl command-info HTTP::uri --dialect f5-irules --json
tcl find-legacy rule.irule --json
# iRules-specific lookups live on the f5 CLI:
f5-query irule event-order rule.irule --json
f5-query irule event-info HTTP_REQUEST --json
# Emit bytecode disassembly
tcl dis script.tcl
# Compile to WASM binary (+ optional WAT sidecar)
tcl compwasm script.tcl -o out.wasm --wat-output out.wat
# Emit ANSI-highlighted output (or --format html)
tcl highlight script.tcl --force-colour
# Diff two iRules using compiler structure layers
tcl diff old.irule new.irule --show ast,ir,cfg
# Use compiler explorer views from the same binary
tcl explore script.tcl --show ir,cfg,opt
# Search KCS help docs (optionally scoped by dialect)
tcl help taint analysis --dialect f5-irules
# Show help for the help command itself
tcl help --help
# Emit help search results as JSON
tcl help taint --json
# Derive version ranges for a package's commands from three local release
# snapshots, and validate the result
tcl spec import --snapshot 1.0=rel/1.0 --snapshot 1.2=rel/1.2 --snapshot 2.0=rel/2.0 \
--dialect tcl8.6 --out mylib.tclspec
# ...or from a GitHub repository's release tags
tcl spec import --github tcltk/tcllib --tag-pattern 'tcllib-*' --limit 8 \
--complete-history --out tcllib.tclspec
For iRules input, pass --dialect f5-irules explicitly:
tcl lint rules/ --dialect f5-irules
iRules-specific verbs (event-order, event-info) live on the separate
f5 CLI under the irule verb group — see the F5 BIG-IP CLI section.
For source builds, run make kcs-db first so the tcl help command can query
the bundled KCS SQLite database.
Install the tcl CLI — the released artefact is the native tcl
binary; no Python required.
See INSTALL-cli.md for the one-line curl | sh
installer, manual install steps for macOS/Debian/Ubuntu/RHEL/CentOS/
Fedora, source builds, and shell completion (bash, zsh, fish)
that covers every verb, dialect, optimiser profile, and source-path
glob — the same indexed-source extension set the server walks, from the
one catalogue, rather than a list of its own.

Differential fuzzer (tcl-fuzz)
tcl-fuzz compares a generated Tcl program between two engines and saves a
seeded reproducer whenever their behaviour differs. Build it with
cargo build -p tcl-fuzz. A release pin is a property of the whole pair, not
just the subject: use a pair whose engines can both honour --tcl-version.
# Both native Rust engines emulate the same selected Tcl release.
tcl-fuzz run --reference runtime-rust --subject tclvm --tcl-version 8.6
# A release-aware finding is found and replayed at its recorded release.
tcl-fuzz replay 12345 --reference runtime-rust --subject tclvm
Pinned findings live below a tclX.Y directory, so the same pair and seed at
Tcl 8.6 and Tcl 9.0 cannot overwrite one another. tclsh is a fixed-release
binary and cannot accept --tcl-version; select a matching build with
--tclsh, and the fuzzer verifies its reported release before starting. See
the fuzz-finding triage guide
for replay and investigation details.
Packaging & environments
tcl pkg is a deterministic Tcl package manager using Go-style Minimum
Version Selection and a content-addressable SHA-256 cache. tcl venv creates
isolated virtual environments that pin a specific tclsh version.
# Quick start
tcl venv create .venv # create a virtual environment
source .venv/bin/activate # activate it
tcl pkg init # create tclpkg.tcl manifest
tcl pkg add json 1.0 # add a dependency
tcl pkg install # resolve, fetch, and lock
tcl pkg tree # show dependency tree
tcl pkg verify # check integrity hashes
The manifest is a native Tcl file (tclpkg.tcl) evaluated in a sandboxed
interpreter. The lockfile (tclpkg.lock) is canonical JSON — two runs against
the same manifest produce byte-identical output (aside from the
generated timestamp, which --frozen preserves).
# tclpkg.tcl — example manifest
package myapp
version 1.0.0
license MIT
tcl >=8.6
require json 1.3.5
require http 2.9.8
dev-require tcltest 2.5.5
The LSP server auto-detects tclpkg.tcl projects and venv lib/ directories,
and offers an "Install via tclpkg" quick-fix on missing-package diagnostics.
See docs/kcs/features/kcs-feature-tcl-pkg.md for the full architecture and contracts.
Suppressing diagnostics
Diagnostics can be suppressed at five different scopes. Smaller scope is always better — turning a code off globally hides real problems in future projects.
| Scope | How |
|---|---|
| One command | # noqa: CODE on the line before the command |
| One file | # tcl-lsp: disable=CODE,CODE near the top of the file |
| One project | [diagnostics]\ndisabled = CODE in .tcl-lsp.ini at the workspace root |
| One editor | tclLsp.diagnostics.CODE: false in editor settings |
| Everywhere | [diagnostics]\ndisabled = CODE in the global config file |
Inline — put on the line before the command:
# noqa: W100
expr $x + 1
# noqa: *
eval $user_input
Top-of-file — before the first non-comment line:
#!/usr/bin/env tclsh
# tcl-lsp: disable=W100,O111
Project config — .tcl-lsp.ini at the workspace root (commit with source):
[diagnostics]
disabled = W111, IRULE1005
[optimiser]
disabled = O109
For the complete reference, see
docs/kcs/kcs-howto-suppress-diagnostics.md.
Excluding files from diagnostics entirely
The scopes above turn individual codes off. To turn all diagnostics
off for files matching a glob — for example, .ruff documentation files
containing purely virtual procs — use [diagnostics] exclude in
.tcl-lsp.ini or the global config.ini, one pattern per line:
[diagnostics]
exclude =
docs/**
generated/[a-c]*.tcl
*.ruff
A pattern with a / matches the file's path relative to its workspace
folder root; a pattern with no / matches the file's name at any depth,
gitignore-style. A matching file publishes no diagnostics at all — every
code, not a subset — while hover, completion, navigation, and formatting
keep working on it. The server watches .tcl-lsp.ini, so saving it
re-applies the exclusion list with no restart. See
docs/kcs/kcs-howto-exclude-files-from-diagnostics.md
for the full glob syntax and multi-root behaviour.
Diff and compare views
The analyser never runs on diff content — a diff editor is two real documents rendered side by side, and the modified side of a Git diff from Source Control (or either side of Compare With…) is a real file, analysed like any open file. The squiggles a diff shows are therefore that file's own correct, whole-file diagnostics, and they are shown by default. Because most of those findings predate the change under review, you can optionally hide them while a file is shown only in a diff editor:
"tclLsp.suppressDiagnosticsInDiffEditors": true
The moment the same file is also open in a normal editor — where you might
be editing it — its diagnostics come back, so analysis of files you are
working on is never affected. This is a VS Code display choice only: the
server keeps analysing and no diagnostics are lost. See
docs/kcs/kcs-howto-hide-diagnostics-in-diff-views.md.
Changing how prominent a diagnostic is
Some checks are intentionally quiet — an unused variable (W211), a dead store, or a style hint render at hint severity, a faint underline that is easy to miss. Rather than turn a check off, you can re-level it per code so the editor shows it more (or less) prominently:
{ "tclLsp.diagnosticSeverity.W211": "warning" }
Accepted values are "error", "warning", "information", and "hint", plus
"default" (keep the analyser's built-in severity). This changes only how the
diagnostic is displayed — never whether the analysis runs. Any diagnostic code
can be re-levelled with tclLsp.diagnosticSeverity.<CODE>; combine it with the
tclLsp.diagnostics.<CODE> on/off toggle above. In .tcl-lsp.ini:
[diagnosticSeverity]
W211 = warning
Multi-file projects and package require
In a project with an "entry" file that runs the package requires and then
sources the rest, the individual modules use the required commands without a
package require of their own. The missing-package require check
(W120) does
not flag them:
Automatically — the server builds a workspace
sourcegraph and each module inherits thepackage requires of every file that (transitively)sources it. Only literalsource path.tcltargets are followed.Explicitly — when the entry file uses a computed
sourcepath (or you prefer to pin it), list the entry files in.tcl-lsp.ini; their combined requires then apply project-wide and the automatic path is turned off:[project] entryPoints = main.tcl src/app.tcl
Diagnostic and optimiser codes
Every code has its own page — what it means, why the check exists, a triggering example, and the fix:
- Diagnostic codes — the E, W, S, T, and IRULE families
- Optimiser codes — the O family
- Per-code KCS pages — one note per code
iRules-only codes are listed in README-f5.md.
Configuration
Every setting is available in each editor's own settings UI, and in an INI file for editor-independent configuration.
- Which settings exist, and what they do —
tclLsp.*reference - Where the server reads configuration from, and which layer wins — kcs-qa-how-tcl-lsp-loads-configuration.md
- Valid INI sections and keys — kcs-qa-what-config-sections-are-valid.md
- Picking a dialect — kcs-feature-dialect-selection.md
- Formatter options — kcs-feature-formatting.md
In VS Code, Tcl: Export Settings writes your current configuration out as an INI file you can commit alongside the project.
Screenshots
Diagnostics & quick fixes


Hover & completions


Security taint analysis

Semantic highlighting

Building and contributing
- A Rust toolchain (current stable) via rustup. The
workspace tracks the floating
stablechannel; current stable is 1.98.0, released 2026-08-18. - Node.js 24+ with npm (pinned to v12 via
packageManager; runcorepack enable npm) - VS Code 1.93+
git clone https://github.com/bitwisecook/tcl-lsp && cd tcl-lsp
make test # the whole suite
make build-editor-vsix # build the VS Code .vsix
make help lists every target. For the development workflow — the gates to run
before pushing, how to add a diagnostic or a formatter option, the repository
layout, and the code-style rules — see AGENTS.md and
CONTRIBUTING.md.
Acknowledgements
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
AI
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.
License
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.