Skip to content

Migrate LSP server from TypeScript to Rust #109

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: LowideaFar-future concept, not yet plannedlspLanguage Server Protocol relatedneeds-prdFeature-sized: requires a PRD before implementation (run /prd)performancePerformance optimization needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions