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
- Stand up
controlforge-syntax crate with logos lexer + typed AST, port parser tests first
- Stand up
controlforge-lsp with tower-lsp, initially returning empty results - validate spawn/stdio wiring end-to-end
- Port providers one by one; run existing unit tests against the new server via the LSP protocol
- Stand up
controlforge-hir with simple re-index on change
- Delete
src/server/ once feature parity is confirmed
- 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
Concept
Replace the TypeScript LSP server (
src/server/) with a native Rust binary. The VS Code extension host (TypeScript shell) stays as-is, since thevscodemodule only runs in Node.js. All language intelligence moves to Rust.Motivation
cargo fuzzfor parser robustness without any extra toolingSuggested Approach
Keep in TypeScript (unchanged)
src/extension.ts- activation, command registrationsrc/client/lsp-client.ts- spawns the Rust binary as a stdio child process, wiresvscode-languageclientRewrite in Rust (one crate per concern)
controlforge-syntax- lexer + parser producing a typed ASTcontrolforge-hir- typed IR + symbol index; simple full re-index on file changecontrolforge-lsp-tower-lspserver, 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 torowanwhen 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:
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
linux-x64,win32-x64,darwin-x64,darwin-arm64.vsixunderbin/- binary resolver inlsp-client.tspicks correct one at runtimecargo-xtaskor ajustrelease scriptMigration Path
controlforge-syntaxcrate withlogoslexer + typed AST, port parser tests firstcontrolforge-lspwithtower-lsp, initially returning empty results - validate spawn/stdio wiring end-to-endcontrolforge-hirwith simple re-index on changesrc/server/once feature parity is confirmedrowanlossless CST when formatting provider becomes a focusRisks
Decision Criteria
Not worth doing until at least one of: