Hello, world! – Rust Glancer

Ik wil een project presenteren waar ik de afgelopen 4 maanden aan heb gewerkt: een alternatieve Rust LSP-implementatie die is gebouwd met een focus op een laag geheugengebruik.

Het heeft twee hoofdfuncties:

  • Het kan zeer weinig geheugen gebruiken (streefdoel <100 MB voor redelijke projecten). Hieraan zijn enkele kanttekeningen verbonden, die hieronder worden beschreven.
  • Het maakt onmiddellijke indexering na herstart mogelijk: als je project was geïndexeerd, is bij het herstarten van de editor geen nieuwe indexering nodig.

Opmerking: gedurende de video in de brontekst bleef het gebruikte RAM onder de 100 MB.

Deze functies maken Rust Glancer geschikt voor oudere computers; ik heb het getest op mijn oude MacBook Pro M1 uit 2020 met 8 GB RAM, en het werkte behoorlijk goed.

MachineLSPBase indexing (engine bruikbaar)Full indexing
MacBook Pro M4 Max, 36GB (2025)Rust Glancer5 seconden8 seconden
MacBook Pro M4 Max, 36GB (2025)rust-analyzer6 seconden13 seconden
MacBook Pro M1, 8GB (2020)Rust Glancer6 seconden9 seconden
MacBook Pro M1, 8GB (2020)rust-analyzer7 seconden14 seconden

Zoals je je kunt voorstellen, is 4 maanden niet veel tijd voor een project zo groot als een Rust LSP. Rust Glancer is nog geen complete LSP; er ontbreekt veel functionaliteit, er zijn enkele bekende bugs en er zijn veel zaken die ik wil verbeteren.

Tegelijkertijd is het al behoorlijk capabel: het heeft een volledige indexeringspijplijn met type-inferentie en een trait-solver (chalk). De meeste "normale" Rust-syntaxis wordt ondersteund en de meeste "normale" LSP-acties werken ook: goto definition, hover, inlay hints, completions, enzovoort.

Wie geïnteresseerd is, kan het al uitproberen via de VS Code-extensie of door de vsix uit de repository te bouwen en te installeren.

Het restant van dit artikel bevat de geschiedenis van het project: motivatie, het gebruik van LLM's, plannen en de roadmap.

Het verschil met rust-analyzer

Er zijn verschillende redenen waarom rust-analyzer veel geheugen verbruikt:

  1. Rust-workspaces bevatten simpelweg veel informatie die geïndexeerd moet worden: duizenden functies, structuren, traits, relaties daartussen, functielichamen en de statements daarin, etc. Elk hiervan moet worden geanalyseerd en onthouden; je kunt hier niet echt shortcuts nemen als je functies wilt hebben zoals "zoek alle referenties naar deze structuur".
  2. rust-analyzer gebruikt salsa als database. Dit is een incrementele query-gebaseerde database die alle benodigde data lazy berekent zonder alles expliciet te hoeven "registreren". Het is een zeer interessante aanpak, maar het is inherent gebonden aan het geheugen, wat het moeilijk maakt om delen van de data van het geheugen naar een andere plek te verplaatsen.
  3. rust-analyzer gebruikt rowan voor de representatie van de syntaxisboom. Het voordeel hiervan is dat het partiële invalidatie mogelijk maakt: als slechts een deel van het bestand is gewijzigd, hoeven alleen de relevante delen opnieuw te worden geparsed, wat sneller is dan het hele bestand bij elke toetsaanslag opnieuw parsen. Echter, de boomstructuur-representatie daarin kan leiden tot zware geheugenfragmentatie (wat betekent dat de hoeveelheid RAM die van het OS wordt ingenomen hoger is dan de hoeveelheid "daadwerkelijk gebruikte" RAM).

Punt (1) is iets waar we mee moeten leven (hoewel er enkele optimalisaties mogelijk zijn die Rust Glancer toepast), maar (2) en (3) zijn consequenties van de architectuur van rust-analyzer. rust-analyzer heeft hiervoor gekozen om de LSP sneller te maken, en dat werkt voor dat specifieke doel.

Het idee dat ik had toen ik aan het project begon: wat als we niet proberen een incrementele LSP te maken? Wat als we alleen een bevroren analyseresultaat hebben dat ongeldig wordt bij het opslaan? Dit zal overduidelijk niet zo snel zijn als rust-analyzer, maar het geeft ons de eigenschappen die we zoeken:

  • Analyseresultaten kunnen naar het bestandssysteem worden verplaatst en alleen in het geheugen worden geladen wanneer ze daadwerkelijk nodig zijn.
  • Opgeslagen analyses zijn herbruikbaar, en omdat ze naar het bestandssysteem zijn verplaatst, kunnen ze na het herstarten van de editor opnieuw worden gebruikt.

Dit is de kernidee van Rust Glancer. Het indexeert de workspace één keer en bewaart de resultaten op het bestandssysteem. Wanneer queries iets nodig hebben, kunnen ze de vereiste informatie laden voor de duur van de query.

Dit is echter niet gratis: een bevroren workspace-analyse is per definitie trager dan een lazy incrementele analyse, aangezien het laden en deserialiseren van data vanaf het bestandssysteem trager is dan laden uit het geheugen. Om dat te verzachten, gebruikt Rust Glancer enkele trucjes. Wanneer je typt, voert het bijvoorbeeld geen volledige analyse uit bij elke toetsaanslag, maar probeert het een ondiepe analyse van het huidige body uit te voeren en hergebruikt het de vorige volledige index. Dit maakt completions redelijk snel, maar het betekent ook dat nieuwe items (imports, structuren, traits) niet worden "geïndexeerd" totdat je het document opslaat. Hopelijk is dit geen probleem: je raakt het snel gewend en in mijn geval voelt het na een tijdje niet meer vreemd.

Voor mensen die vertrouwen op agentic workflows is Rust Glancer ook geoptimaliseerd voor een grote hoeveelheid wijzigingen buiten de editor. In rust-analyzer heb ik geobserveerd dat wanneer agents de code bewerken, inlay hints uit positie kunnen raken. Ik had aanvankelijk hetzelfde probleem in Rust Glancer, maar dit is opgelost door een aangepaste file watcher te implementeren en deze te finetunen. De server heeft daarnaast een lagere prioriteit voor wijzigingen buiten de editor, zodat agentic wijzigingen niet leiden tot constante her-indexering.

Toch is het belangrijk om te begrijpen dat Rust Glancer voordelen heeft, maar ook nadelen (naast het feit dat het onvolledig is) vergeleken met rust-analyzer. Het is zeer onwaarschijnlijk dat Rust Glancer ooit "precies als rust-analyzer, maar dan beter" zal worden. Ik stel me voor dat rust-analyzer de standaardkeuze blijft voor projecten waarbij volledigheid en nauwkeurigheid per toetsaanslag cruciaal zijn, terwijl Rust Glancer werkt voor mensen met zwakkere machines of mensen die bereid zijn tot कुछ concessies om het RAM-gebruik te verminderen.

Hoe en waarom het is gebeurd

Ik schrijf ongeveer 7 jaar professioneel in Rust en ik volg al vroeg hoe de compiler en de bijbehorende tooling worden ontwikkeld. Ik heb bijgedragen aan rustc, clippy en rust-analyzer, en ik heb tientallen uren besteed aan het lezen van de broncode om mezelf dingen aan te leren. Ik was me dus goed bewust van hoe groot een project als een Rust LSP is.

Tegelijkertijd heb ik een lief-haatrelatie met rust-analyzer. Het is absoluut prachtig, behalve op twee punten: het geheugengebruik en de initiële indexering (vooral met build scripts / proc macros ingeschakeld). In mijn geval zijn deze problemen drastisch: ik heb een ongebruikelijke workflow waarbij ik twee identieke IDE's open heb staan op twee schermen met een reeks projecten in een workspace. Hierdoor is het geheugengebruik ongeveer 2N. Bij mijn laatste set projecten verbruikte rust-analyzer 16 GB RAM, wat ik liever voor andere zaken beschikbaar had gehad; niet te noemen dat mijn PC-fans elke keer dat ik VS Code opende op volle toeren draaiden door de vele parallelle indexeringsjobs.

Op een gegeven moment dacht ik dat ik voldoende vertrouwen had in mijn Rust-kennis en dat ik misschien geen volledige LSP nodig had, maar iets simpelers en efficiënter. Ik besloot te proberen "smart ctags voor Rust" te bouwen. Ik wilde expliciet geen alternatieve LSP bouwen, omdat dat een krankzinnige taak is. Weinig wist ik...

De initiële voortgang verliep soepel: ik maakte gebruik van de syntaxisbibliotheek van rust-analyzer, bracht items over naar interne representaties, bouwde definitie-kaarten en modulestructuren, en kreeg alle declaraties geïndexeerd. Het was zo verrassend eenvoudig dat ik besloot om primitieve body lowering toe te voegen. Daarna voegde ik zeer eenvoudige type propagation toe. Toen bleek dat naïeve type-propagatie niet veel opleverde — maar ik had al deze mooie inlay hints, dus ik wilde meer. Ik dacht dat ik niet gaf om complexe gevallen en nightly-functies (echt niet?...). Dus volgde naïeve trait-resolutie via impl-header matching. Het is verslavend.

De illusie verbrak echter toen ik besloot dat het redelijk was om de volgende code te ondersteunen:

fn mul_by_two(vals: &[u8]) -> Vec<u8> {
    vals.iter().copied().map(|v| v * 2).collect()
}

De code is simpel, maar om dit te ondersteunen hebben we het volgende nodig:

  • Slice type support
  • Closures / Fn traits
  • Trait solving
  • Associated type projection
  • Een heleboel nightly-functionaliteiten

Dat laatste is grappig: ik wilde nightly vermijden, maar ik hield geen rekening met het feit dat std (of de sysroot in het algemeen) "ademt" nightly.

Zo transformeerde het project, functie na functie, langzaam van "smart ctags" naar een "echte LSP". De drie grootste mijlpalen waren:

  1. Declaratieve macro-expansie: (Ik haat declaratieve macros nu). Gelukkig kon ik voor het grootste deel de infrastructuur van rust-analyzer hergebruiken.
  2. Een correcte type-inferentie engine: Dit was een "oh wow"-moment toen ik echt besefte hoe type-inferentie werkt (kortgevat: we "linken" alle gerelateerde type-bindings in een grote inferentietabel, en proberen vervolgens bewijs te vinden vanuit alle mogelijke plekken, waarbij het leveren van bewijs types voor meerdere plekken kan oplossen).
  3. Een correcte trait-solving engine: Aanvankelijk schreef ik dat het onwaarschijnlijk was dat er een trait-solver in dit project zou komen, maar ik wilde dat het bovenstaande iterator-voorbeeld correct werkte. Ik verzette me een tijd tegen de integratie van een trait-solver en probeerde naïeve hacks, maar dat werd steeds complexer en werkte slecht. Uiteindelijk gaf ik toe en integreerde ik Chalk, wat aanzienlijk eenvoudiger bleek dan de hiërarchie die ik zelf had gebouwd. Chalk snel maken was echter een andere uitdaging.

Los hiervan ben ik het meest trots op de profiling stack die prestaties en geheugengebruik kan meten (zowel natief, door daadwerkelijk gealloceerde objecten te volgen, als met jemalloc), data on-demand kan profileren en de LSP kan vergelijken met rust-analyzer, inclusief benchmarks in CI.

Ongeveer anderhalve maand geleden ben ik Rust Glancer als mijn dagelijkse tool gaan gebruiken in plaats van rust-analyzer. Nu ben ik tevreden genoeg met de staat ervan om het aan een breder publiek te presenteren.

Gebruik van LLM's

Dit project is gebouwd met intensief gebruik van LLM's. Het is echter niet "vibe coded".

Ik verifieer elke pull request om er zeker van te zijn dat ik tevreden ben met de staat van de codebase. In de git-historie zijn PR's te zien met 10k+ regels diff, maar deze zitten dagen uit elkaar, ondanks het feit dat ik bijna elke dag aan dit project werk. Ik geef om de code; het zou vreemd zijn als ik 4 maanden in een Rust LSP zou steken zonder veel naar de code te kijken.

Ik doe niet alsof ik een ervaren LSP-ontwikkelaar ben en dat de code perfect is. Het is in een staat waarin ik ermee kan werken, maar ik begrijp dat sommige delen misschien niet idiomatisch zijn voor compiler-tooling design. De code bevat veel commentaren; ik heb mijn best gedaan om deze behulpzaam te maken omdat ik ze constant moet lezen. De kwaliteit is waarschijnlijk niet zo goed als professioneel geschreven menselijke documentatie, maar naar mijn mening is het nuttig en niet irritant om te lezen.

Een groot deel van de reis is leren. LLM's kunnen goede domeinexperts zijn en weten veel meer over LSP-design dan ik. Tegelijkertijd zijn LLM's niet goed in het bouwen van grote projecten. De volgende loop herhaalde zich meerdere keren tijdens de ontwikkeling:

  1. Ik bouw iets nieuws.
  2. LLM-voorstellen lijken redelijk, dus ik volg ze.
  3. Het werkt, maar er is iets dat me stoort.
  4. Ik denk na over het ontwerp en ontdek een grote fout.
  5. Ik werk met de LLM om dit te herstellen (soms een week lang, als de fout bijzonder groot was — maar hoe groter de fout, hoe meer ik leer).

Aan de ene kant zou ik kunnen klagen dat LLM's het project vele malen probeerden te ontsporen. Maar aangezien het mijn code is, denk ik dat de code op sommige momenten slechter wordt, maar dat ik deze verbeter naarmate ik meer leer. Dat is een normaal software-ontwikkelingsproces, alleen versneld.

Kortom, LLM's zijn slechts een hulpmiddel; het is de keuze van de gebruiker om ze verantwoord in te zetten of het denken eraan uit te besteden. Gezien de huidige "heksenjacht" heb ik één verzoek: reduceer me niet tot een "clanker". Het is mijn code, dus als je het als "slop" beschouwt, noem het dan mijn slop, niet die van de AI.

Ik sta open voor kritiek en feedback: hoe meer ik leer, hoe meer ik de codebase kan verbeteren. Of ik daar LLM's voor gebruik of niet, maakt in mijn ogen niet veel uit.

Wat volgt er nu

Het project is al in een staat waarin het voor sommige gebruikers als dagelijkse tool kan dienen, maar ik heb grotere plannen. In komende releases kun je het volgende verwachten:

  • Verdere prestatie-optimalisaties.
  • Meer geheugenoptimalisaties (voornamelijk tijdens indexering, plus het oplossen van enkele fragmentatieproblemen na een volledige indexeringsronde).
  • Verbeterde type-inferentie en syntaxis-ondersteuning.
  • Code-acties (zoals het implementeren van ontbrekende trait-velden, auto-imports, etc.).
  • Potentieel ondersteuning voor proc-macros (ik heb een ongebruikelijk idee dat geen daadwerkelijke code-executie vereist, maar dat kost tijd om voor te bereiden).

Sommige functies zullen waarschijnlijk niet worden ondersteund, zoals build scripts of proc-macros via proc-macro-aanroep (alles wat onbetrouwbare code-executie vereist). Ook ben ik niet van plan om te werken aan zaken die momenteel onnodig zijn, zoals de migratie naar de nieuwe trait-solver. Niche-zaken zoals specifieke nightly-functies worden waarschijnlijk uitgesteld tot het project een zekere mate van volwassenheid heeft bereikt met stable Rust.

Daarnaast heb ik enkele technische trucjes toegepast in Rust Glancer waar ik trots op ben (zoals het uitlijnen van allocatie-levensduuren om geheugenfragmentatie te verminderen, een engine-as-a-subprocess model om te helpen met fragmentatie en multi-workspace projecten, en een sharded cache). Als er interesse in is, schrijf ik hier graag blogposts over.

In ieder geval hoop ik dat het project nu al nuttig kan zijn voor sommigen, en in de toekomst voor meer mensen.