This project extends the Midnight Network with additional developer tooling.
⚠️ Note: This is an experimental project developed in my personal free time for educational purposes — primarily to learn how Language Server Protocol implementations work. While functional, it is not officially supported. Feedback and contributions are welcome!
Language Server Protocol implementation for the Compact smart contract language (Midnight network).
Supports Compact language 0.23.0 – 0.27.0 (toolchains 0.31.1 – 0.35.0). The Mainnet / Preprod / Preview pin is 0.31.1 (language 0.23.0); 0.34.0 and 0.35.0 (language 0.26 / 0.27, ledger 9) are supported for the upcoming network upgrade.
| Feature | Description |
|---|---|
| Diagnostics | Real-time syntax errors + compiler errors on save |
| Semantic Tokens | Rich syntax highlighting (functions, types, parameters, etc.) |
| Completion | Keywords, types, snippets, local and imported symbols |
| Hover | Documentation for keywords, types, and symbols |
| Go to Definition | Jump to symbol definitions (local and imported) |
| Find References | Find all usages of a symbol (local and cross-file) |
| Rename | Rename symbols across the workspace |
| Signature Help | Parameter hints while typing function calls |
| Document Symbols | Outline view (circuits, structs, enums, modules) |
| Formatting | Whole-document and range (selection) formatting via format-compact |
| Folding Ranges | Code folding for blocks and functions |
| Cross-file Errors | Errors propagate to dependent files on save |
| Version-aware symbols | Registry suggestions follow the detected compiler's language version |
| Cross-contract calls | Completion, hover, go-to-definition, signature help, find-references, and rename for ref.circuit(...) — reached through struct fields (w.token.circuit(...)), call bases (make().circuit(...)), method-call bases (factory.create().circuit(...)), index bases (items[i].circuit(...)), fields on a method result (items.head().value.circuit(...)), and generic calls/fields (pick<T_Token>(m).circuit(...), Box<Token>.value.circuit(...)); contract implements type completion |
| Struct & enum members | Find References and Rename for struct fields (w.balance) and enum variants (Phase.day), gated by the owner type (including imported structs/enums) |
| Selective imports | import { Token, Wallet as W } from "./mod" binds symbols (including aliases) explicitly, so imported types resolve to the right file instead of the ambiguous by-name workspace scan |
Works with Compact's import system:
import "./Utils" prefix Utils_;
Utils_add(5, 5); // Completion, hover, go-to-def, find refs, rename, signature help all work
On ledger-9 toolchains, a contract reference (ref: Token) supports the full editor feature set
on the circuits declared by its contract type:
contract Token {
circuit transfer(to: Address, amount: Uint<64>): Boolean;
}
contract implements Token; // completion offers `Token`
circuit useIt(ref: Token, to: Address, amount: Uint<64>): Boolean {
return ref.transfer(to, amount); // completion, hover, go-to-def, signature help
}
The reference's type is resolved syntactically: from a written annotation (ref: Token), through
a local alias (type MyToken = Token;), or inferred from the initializer of a const
(const t = make(); uses make's declared return type — including when make is defined in
another file, whose scope decides what the return-type name means). The contract type itself may
live in another file: import it (import "./Token"; or import "./Token" prefix T_;) and the
features work across files — including when the defining file is not open, resolved from the
workspace scan. Member-chain bases resolve through struct fields (w.token.transfer(...)),
including nested (a.wallet.token.transfer(...)) and imported structs. Expression bases resolve
too: make().transfer(...) uses the callee's return type and items[i].transfer(...) uses the
container's element type (Map<K, V> → V, Vector<n, T> → T, …). Method-call bases resolve the
receiver first, then the method's return type — a contract circuit's declared return
(factory.create().transfer(...)) or a built-in method's return with generics substituted
(tokens.lookup(k).transfer(...), where Map.lookup returns V). Fields on a method result also
resolve (items.head().value.transfer(...): List<T>.head() → Maybe<T>, whose value field is
T). Generic parameters are read from explicit call-site type arguments
(circuit pick<T>(m: Maybe<T>): T used as pick<T_Token>(m) resolves to T_Token) and, as a
superset of the compiler, inferred when an argument's type directly matches the parameter
(circuit id<T>(x: T): T used as id(token) resolves to token's type); generic struct fields
substitute (Box<Token>.value → Token). Find-references and rename follow a circuit across every
file where a contract-typed reference uses it. contract implements requires language 0.26+.
Note on generics. Compact only supports type parameters on declarations —
struct,typealiases, top-levelcircuit(functions),witness, andmodule. Contracts, contract circuits, and member (.method) calls cannot be generic, so there is no such thing as a generic "method" in the language. Generic calls require explicit type arguments (id<Uint<64>>(x)); the compiler does not infer them.
| Area | Status |
|---|---|
| Code Actions | Not implemented |
| Contract-reference typing | Heuristic, not a type system: resolves parg / const / ledger annotations, follows type X = Y aliases, walks struct fields (w.token.circuit(...)), resolves call bases (make().circuit(...)), method-call bases (factory.create().circuit(...), tokens.lookup(k).circuit(...)), index bases (items[i].circuit(...)), and field access on a method result (items.head().value.circuit(...)), and infers const t = make(); from the callee's declared return type (same file, or an imported/workspace callee resolved in its own file scope). Generic parameters come from explicit call-site type arguments (first<Uint<64>>(m)) or, as a superset of the compiler, from an argument whose type directly matches the parameter (id<T>(x: T) as id(token)), and generic struct fields substitute (Box<Token>.value); generic methods are not a Compact construct |
- Rust toolchain 1.90+ (for building; pinned in
rust-toolchain.toml) - Compact devtools (
compact) with a toolchain in 0.31.1 – 0.35.0 — install the one you target withcompact update 0.31.1(or0.34.0/0.35.0). Providescompactc(diagnostics) andformat-compact(formatting).
The LSP reads the compiler's language version and gates registry symbols so one registry serves both the mainnet compiler and newer toolchains:
| Toolchain | Language | Ledger | Deployable to public networks |
|---|---|---|---|
| 0.31.1 | 0.23.0 | 8 | ✅ Mainnet / Preprod / Preview (target) |
| 0.34.0 | 0.26.0 | 9 | ❌ |
| 0.35.0 | 0.27.0 | 9 | ❌ |
Registry entries carry a min_language_version and, for symbols removed in a later language
version, an exclusive max_language_version. One registry therefore serves both the mainnet
compiler and newer toolchains: a symbol is hidden when the detected compiler is below its minimum
(e.g. keccak256 on 0.23.0) or at/after a removal. When no compiler is found, gating is disabled
and all symbols are shown. On toolchains where the ZKIR-v3 backend is opt-in
(0.34.100 – 0.35.104), the LSP automatically passes --feature-zkir-v3 so the foreign-curve
symbols resolve. New syntax is supported too: contract implements and emit(...).
Upstream release notes: https://docs.midnight.network/relnotes/compact; full version map and
migration notes: MIGRATION_COMPILER_0.31.1.md and
CLAUDE.md.
Install the language server from crates.io:
cargo install compact-lspPrebuilt binaries for Linux, macOS (Intel + Apple silicon), and Windows are on the releases page.
Or build from source:
cargo build --release
# binary at target/release/compact-lspTwo crates are published:
| Crate | What it is | Docs |
|---|---|---|
compact-lsp |
the language server binary (cargo install compact-lsp) |
— |
compact-analyzer |
reusable analysis library (diagnostics, formatter, parser) | docs.rs |
The LSP auto-detects the compactc compiler:
COMPACT_COMPILERenvironment variable- Compact devtools active toolchain
$COMPACT_DIRECTORY/bin/compactc(default~/.compact/bin/compactc; the symlinkcompact update <ver>repoints) — resolved to the realcompactc.bin ~/compactc/compactc.bincompactc.bin/compactcinPATH
The detected language version gates registry symbols: each entry carries a min_language_version
and an exclusive max_language_version, so symbols added after 0.23.0 (e.g. keccak256) or removed
in a later version are hidden appropriately. When no compiler is found, gating is disabled and all
symbols are shown. On toolchains where the ZKIR-v3 backend is opt-in (0.34.100 – 0.35.104), the LSP
passes --feature-zkir-v3 automatically. format-compact is found the same way
(COMPACT_FORMATTER, then the devtools bin/, then PATH).
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo build --release
cargo testGitHub Actions runs these on main and dev, plus a compiler-probe matrix over toolchains
0.31.1, 0.34.0, and 0.35.0 that installs each toolchain and runs the full suite with
COMPACT_LSP_REQUIRE_COMPILER=1 / COMPACT_LSP_REQUIRE_FORMATTER=1 (a missing toolchain fails
instead of skipping), and a report-only coverage job (cargo llvm-cov). The end-to-end LSP
tests spawn the server binary, speak JSON-RPC over stdio, and are backed by compiler-validated
fixture projects; see TESTING.md for the layers, snapshots, property tests, and
known issues.
- Testing — see TESTING.md for the test layers, fixtures, snapshots, and known issues.
- Changelog — see
CHANGELOG.mdfor release notes. - Releasing — see
RELEASING.md; cut a release withscripts/release.sh. - Upstream tracking — see
UPSTREAM_RELEASES.md; the weeklyupstream-watchworkflow files an issue when a newer Compact toolchain ships.
- compact-tree-sitter - Tree-sitter grammar for Compact (published on crates.io as
tree-sitter-compact)
MIT