
You may read my paper and cite like this:
WANG, Z. (2025, May 12). Scheme-langserver: Treat Scheme Code Editing as the First-Class Concern. The 18th European Lisp Symposium (ELS`25), Zurich. https://doi.org/10.5281/zenodo.15384882
Due to occasional GitHub access restrictions from China, this repository is also mirrored on Codeberg and Gitee. I collaborate with XmacsLabs; a fork is available here.
VSCode is now supported! See the setup guide.
Note: Auto-generated type information is available here. It is mainly used for downstream development and debugging.
Implementing IDE features like autocomplete, goto definition, and hover documentation is a significant effort. Compared to languages like Java, Python, JavaScript, or C, language server implementations for Lisp dialects are still scarce. Existing tools such as Geiser, racket langserver, and swish-lint rely primarily on a REPL or keyword tokenization rather than static program analysis.
For example, when editing an incomplete project whose code is not yet fully executable, Geiser can only complete top-level bindings listed by environment-symbols (on Chez Scheme) or raw symbols—not true identifiers. This means local bindings and unfinished code receive no help in recognizing valid identifier scopes. The same limitation applies to goto definition and other core IDE features.
The root cause is that Scheme and other Lisp dialects present a formidable challenge for program analysis: their rich data structures, flexible control flow, and especially macros make static reasoning difficult. But this does not mean Scheme is only for geniuses and meta-programming. With a better editing environment, Scheme can be accessible and productive for everyone.
scheme-langserver is a Language Server Protocol (LSP) implementation for Scheme that provides completion, goto definition, hover, and type inference through static code analysis based on the R6RS standard, with switchable top environments for R6RS, R7RS, S7, and Fluent Scheme (Ansys Fluent’s embedded Petite Chez Scheme). It handles incomplete code gracefully and is published via Akku, a Scheme package manager.
The server has been tested on Chez Scheme 9.4, 9.5, and 10.x.
See the setup guide.
scheme-langserver can persist the analyzed workspace state to a Chez FASL cache so
that subsequent restarts skip the expensive init-references phase. Enable it
with the --cache-path option:
./run --cache-path ~/.cache/scheme-langserver
The cache file is written to <cache-path>/workspace.fasl. It is saved when the
server receives a normal LSP exit or shutdown request; if the process
crashes or is killed, the previously saved cache remains usable. The cache is
keyed by a manifest that includes the langserver version, Chez version, machine
type, and record-layout fingerprint; any mismatch falls back to a cold start.
Because Chez FASL is platform-specific, do not share the cache file across
machines or Chez versions. When only a few files have changed since the cache
was saved, the server performs an incremental refresh and preserves the analysis
results for unchanged files.
Typical numbers (measured 2026-10-30, min of 3 runs, identifier analysis only, macro resolution off):
| Fixture | Cold startup | Cached startup | Speedup |
|---|---|---|---|
| simple-lib (2 files) | ~3 ms | ~2 ms | ~1.5x |
| Synthetic 100 independent simple-lib copies (200 files) | ~407 ms | ~49 ms | ~8x |
| scheme-langserver itself | ~0.1–0.4 s | ~300 ms | ~break-even |
Since the 2026-10 analysis caches (import-set caching, negative caching of unexpandable call sites, expansion budgets), cold initialization is fast enough that the cache is close to break-even at init for small workspaces — the FASL read plus the per-file mtime sweep costs about as much as re-analysis. It still pays off on larger workspaces and through persisted diagnostics, which are republished immediately after a cache load without re-diagnosing files.
See doc/analysis/workspace.md §8 and AGENTS.md §11 for implementation details.
By default scheme-langserver uses the Akku package manager’s file list to decide which files belong to the workspace. For projects without Akku, or for non-standard file extensions, choose one of the following mutually exclusive options:
--package-manager (or -p) selects a preset:
./run -p akku # Akku-managed files (default)
./run -p txt # Only .scm.txt files
--file-filter (or -f) gives an exact extension list. It can be repeated
and the extensions are merged automatically; leading dots are optional:
./run -f ".sls,.scm,.ss,.sld,.scm.txt"
./run -f scm.txt -f scm -f sls
./run -f sls -f scm -f sps -f ss -f sld
-p and -f cannot be used together. The resolved filter is persisted in the
workspace-cache manifest, so changing the filter invalidates any previously saved
cache.
For troubleshooting tips, see debugging.md.
2.1.11 — Macro release: automatic macro resolution is rebuilt on Chez’s own expander IR (route-2 pipeline retired), now covering match-index-node and any library-resolved user macro, opt-in via -a/--auto-expand; macro-aware type inference with resource controls makes whole-repo type inference feasible (was OOM); macro expansion in compiled binaries fixed; the ~38 s cold-start issue resolved.
2.1.10 — Bug-fix release hardening the tokenizer: files with content after #!eof and files whose first datum is not a parenthesized form no longer fail the whole workspace initialize; adds the Fluent Scheme top environment.
Active development is focused on bug fixes, performance profiling, and expanding the type inference system.
See doc/release-history.md for older releases.




workspace/symbol).define-syntax, let-syntax, and other macro forms via hand-written rules.-a / --auto-expand). Macro call sites are expanded by Chez’s own expander and the resulting IR is mined for bindings and references — macro-agnostic, covering the ufo-match family, match-index-node, and any library-resolved user macro. Paired with expansion-driven macro type inference that closes the value-flow chain through pattern variables. See the mechanism walkthrough..sps files.length-a and length-b (both integer?) appear first because they match the parameter type required by <=.

send-message
2023 11 21 11 26 41 967266866
{"jsonrpc":"2.0","id":"3","result":[{"label":"length-a"},{"label":"length-b"},{"label":"lambda"},{"label":"latin-1-codec"},{"label":"lcm"},{"label":"least-fixnum"},{"label":"length"},{"label":"let"},{"label":"let*"},{"label":"let*-values"},{"label":"let-syntax"},{"label":"let-values"},{"label":"letrec"},{"label":"letrec*"},{"label":"letrec-syntax"},{"label":"lexical-violation?"},{"label":"list"},{"label":"list->string"},{"label":"list->vector"},{"label":"list-ref"},{"label":"list-sort"},{"label":"list-tail"},{"label":"list?"},{"label":"log"},{"label":"lookahead-char"},{"label":"lookahead-u8"}]}
.scm, .ss, .sps, .sls, .sld.source and code fields. Detects library-not-found, duplicate identifiers in binding forms (e.g. (lambda (x x) ...)), unused imports (e.g. (only (rnrs) car) where car is never referenced), and tokenizer syntax errors.

textDocument/rename + prepareRename).textDocument/formatting).textDocument/signatureHelp).textDocument/codeAction) — e.g. “Remove unused import”, “Organize imports”.Pull requests are welcome! Please see AGENTS.md for project conventions, build steps, and coding style before opening a PR.
Since mid-2025, active development on this project has been assisted by KIMI (Moonshot AI) in a vibe-coding workflow: the maintainer describes intent in natural language, KIMI explores the codebase, proposes changes, and iterates with tests. If you notice commits authored or co-authored by kimi, that is the AI agent trail. Human review and final approval always remain with the maintainer.
Almost all key procedures and APIs are covered by tests. Run the full suite with:
bash test.sh
For faster feedback during development, run a single test file:
source .akku/bin/activate
scheme --script tests/protocol/apis/test-definition.sps
Note: Tests currently focus on single-threaded execution. Note:
tests/robustness/robustness-concurrent.sps(multi-threaded) used to fail intermittently withException in thread-loop: ... no fail-handlerfollowed by a hang. The root cause was a non-tail-recursive wait loop inrequest-queue-pop(each recursive frame dequeued again, consuming tasks twice), diagnosed via cisco/ChezScheme#1055. Fixed in 2.1.10.
Script-Fu is based on Scheme. Using this example, you can apply scheme-langserver to .scm files in GIMP.
Possible future targets include OMN (Opusmodus Notation) and AutoLisp.
find . -name "*.sls" ! -path "./.akku/*" |xargs wc -l
define, lambda, let, define-record-type, etc.define-record-type in the type system