Skip to content

Migrate LSP server from TypeScript to Rust #109

@michaeldistel

Description

@michaeldistel

Concept

Replace the TypeScript LSP server (src/server/) with a native Rust binary. The VS Code extension host (TypeScript shell) stays as-is, since the vscode module only runs in Node.js. All language intelligence moves to Rust.

Motivation

  • Native parsing speed and lower memory footprint as workspace size grows
  • cargo fuzz for parser robustness without any extra tooling
  • Stronger type guarantees for the IEC 61131-3 type system (Rust enums vs TS interfaces)
  • Foundation for incremental re-analysis if scale demands it later

Suggested Approach

Keep in TypeScript (unchanged)

  • src/extension.ts - activation, command registration
  • src/client/lsp-client.ts - spawns the Rust binary as a stdio child process, wires vscode-languageclient

Rewrite in Rust (one crate per concern)

  • controlforge-syntax - lexer + parser producing a typed AST
  • controlforge-hir - typed IR + symbol index; simple full re-index on file change
  • controlforge-lsp - tower-lsp server, implements all current providers (completion, definition, rename, formatting, diagnostics, hover). Stdio transport, spawned by the TS shell.

Stack rationale

tower-lsp - the standard Rust LSP framework. Async, well maintained, no real alternative worth considering.

logos - compile-time DFA lexer generator. Low complexity cost; less code to maintain than a hand-rolled lexer and harder to get wrong. Justified.

rowan (lossless CST) - preserves whitespace and comments, needed for a correct formatting provider. The main tradeoff: more complex than a simple typed AST, with a meaningful learning curve. Justified long-term but not required upfront. Start with a typed AST; migrate to rowan when formatting becomes a focus.

salsa (incremental computation) - not needed at migration time. See scale analysis below. If ever required, ra-salsa (rust-analyzer's actively maintained fork) is the best option.

On scale and performance

ST projects rarely reach the scale where naive full re-index on save becomes perceptible:

Project size Files Lines TS LSP Rust (no salsa)
Typical machine 20-50 5k-20k Fine Fine
Large OEM 100-300 50k-150k Sluggish on save Fine
Massive platform 500+ 300k+ Noticeable lag Fine
Extreme 1000+ 500k+ Poor Consider salsa

Native Rust re-parses ~100x faster than TypeScript. Even a naive full re-index on every save is imperceptible up to ~500 files. The vast majority of ControlForge users are in the 10-100 file range.

Distribution

  • CI produces platform binaries: linux-x64, win32-x64, darwin-x64, darwin-arm64
  • Bundled in .vsix under bin/ - binary resolver in lsp-client.ts picks correct one at runtime
  • Highest ongoing maintenance cost of the migration; automate with cargo-xtask or a just release script

Migration Path

  1. Stand up controlforge-syntax crate with logos lexer + typed AST, port parser tests first
  2. Stand up controlforge-lsp with tower-lsp, initially returning empty results - validate spawn/stdio wiring end-to-end
  3. Port providers one by one; run existing unit tests against the new server via the LSP protocol
  4. Stand up controlforge-hir with simple re-index on change
  5. Delete src/server/ once feature parity is confirmed
  6. Migrate to rowan lossless CST when formatting provider becomes a focus

Risks

  • Platform binary distribution adds CI complexity and release friction
  • Contributor bar rises significantly - Rust knowledge required for all LSP work
  • Rewrite cost is substantial; no user-visible features ship during migration

Decision Criteria

Not worth doing until at least one of:

  • Measurable performance complaints on real workspaces
  • A specific feature is blocked by the TypeScript architecture
  • A Rust-fluent contributor is available to lead it

Metadata

Metadata

Assignees

No one assigned

    Labels

    ideaFar-future concept, not yet plannedlspLanguage Server Protocol relatedperformancePerformance optimization needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions