Ga naar inhoud

Code-intelligentie

Control Center geeft een agent vier manieren om code te begrijpen buiten het alleen lezen ervan. Ze beantwoorden echt verschillende vragen, en de reden dat het er vier zijn in plaats van één is dat geen enkel mechanisme ze allemaal beantwoordt.

Mechanisme Beantwoordt Kosten
Codegraph Wat hier staat, en wat ervan afhangt Een indexeringsronde; goedkoop om te bevragen
Taalserver Of dit echt compileert, en wat dit type is Een serverproces per project
Structureel zoeken Waar deze VORM voorkomt Een parse per bestand
Debugger Wat op dit moment waar was, in dit frame Een gestopt proces

De codegraph is gebouwd op tree-sitter: symbolen, aanroeprelaties, een impactradius. Hij is snel, hij is goedkoop, en hij weet niets van typen. Hij kan je niet vertellen of de aanroep die je net hebt geschreven compileert, of dat veld op het object bestaat, of dat je hernoeming een herexport heeft gemist.

Zonder taalserver komt een agent daar pas achter tijdens het testen — of nooit. Hij schrijft iets wat aannemelijk lijkt, gaat verder, en de fout duikt drie beurten later op, vastgeplakt aan een andere wijziging.

De winst zit dus niet echt in de lsp-tool. Die zit erin dat write, edit en apply_patch zijn omhuld: na elke schrijfactie gaat het bestand door de taalserver en wordt nieuw geïntroduceerde diagnostiek in het resultaat van de tool verwerkt. De agent merkt dat hij de build heeft gebroken op het moment dat hij hem breekt, of hij er nu aan dacht om ernaar te vragen of niet.

Herhalen wat de agent al te horen heeft gekregen is erger dan niets zeggen: het vult het resultaat met ruis waar de agent overheen leert lezen, en de ene nieuwe fout verdwijnt daarin mee. Een register per run houdt bij wat er is gemeld, met ernst, code en bericht als sleutel — niet de positie, zodat een diagnose die alleen vier regels is opgeschoven niet opnieuw als nieuw wordt gemeld.

Geen enkele server start bij het opstarten. Er start er één bij het eerste verzoek dat hem nodig heeft, wat bijna altijd de eerste bewerking is van een bestand dat hij opeist, en inactieve servers worden opgeruimd. Een host die nooit Dart aanraakt, start nooit een analyzer.

Detectie is een doorsnede: er moet een marker in de root van de checkout staan en de binary moet te vinden zijn. Alleen een marker zegt “dit project is Python” en niets over of de server is geïnstalleerd; alleen een vindbare binary zegt dat de host een toolchain heeft en niets over deze checkout. Alleen beide samen betekenen dat een query kan slagen, en een tool die wordt aangeboden en altijd faalt is erger dan een die er niet is.

Waarom structureel zoeken, als grep al bestaat

Sectie getiteld “Waarom structureel zoeken, als grep al bestaat”

grep matcht tekens. Dat is het juiste gereedschap voor een naam en het verkeerde voor een vorm.

Een structureel patroon wordt geparseerd met dezelfde grammatica als het bestand, dus dispose($X) vindt de aanroep hoe die ook is gespatieerd, afgebroken of over regels verdeeld, en matcht nooit dezelfde tekens binnen een letterlijke tekenreeks of commentaar.

De eigenschap die het meer maakt dan een regex met extra stappen is dat een metavariabele die in één patroon wordt herhaald dezelfde tekst moet vastleggen:

if ($X != null) $X.dispose()

matcht een controle die opruimt wat hij heeft getest, en matcht niet een controle die iets anders opruimt. Dat is een klasse bugs die je tekstueel helemaal niet kunt uitdrukken.

Het draait op de tree-sitter die al gelinkt is

Sectie getiteld “Het draait op de tree-sitter die al gelinkt is”

Er is geen tweede engine. cc_natives laadt tree-sitter en de grammatica’s al voor de codegraph, met een cache voor gecompileerde query’s en een tabel met talen. De matcher is pure Dart over trees die toch al werden geparseerd. Een tweede structurele engine zou een tweede parser betekenen, een tweede set grammatica’s om op elk platform te bouwen en klaar te zetten, en twee antwoorden op “wat is een aanroepexpressie”.

Een grammatica definieert alleen wat een heel bestand is. Kaal op het topniveau van Dart geparseerd is dispose(x) een volkomen geldige functiesignatuur — een declaratie met de naam dispose die een parameter x aanneemt — zonder dat er ergens een fout is die het verraadt. Een matcher die op die parse is gebouwd, zoekt naar declaraties, vindt geen aanroepen, en meldt “geen resultaten” voor een patroon dat nooit werd gelezen zoals het eruitziet.

Daarom wordt een patroon in een ladder van hulpconstructies geprobeerd — instructie, expressie, klasselid, topniveau — en de eerste die het accepteert wint. Een fragment dat geen enkele context accepteert, wordt gemeld als niet te parsen in plaats van te worden gezocht en stilletjes niet gevonden.

Waarom een herschrijving eerst wordt klaargezet in plaats van meteen geschreven

Sectie getiteld “Waarom een herschrijving eerst wordt klaargezet in plaats van meteen geschreven”

Een structurele herschrijving is de breedste bewerking die een agent kan doen: één patroon, veertig bestanden, en niemand leest het resultaat regel voor regel — niet het model, en niet de persoon die erom vroeg. Hem schrijven en “klaar” melden betekent dat de eerste eerlijke blik op de wijziging achteraf een git diff is.

Daarom rapporteert ast_edit wat het zou doen — het aantal, de bestanden, de gematchte plekken — en wordt de wijziging pas doorgevoerd wanneer resolve dat doet. Dat is ook wat ervoor zorgt dat een mens eerst een diff met de keuze tussen accepteren en negeren te zien krijgt voordat er iets naar schijf gaat.

De doorvoering is alles-of-niets en elk bestand wordt gecontroleerd tegen de inhoud die op het moment van klaarzetten is vastgelegd. Tussen het klaarzetten en het doorvoeren kan de agent een van die bestanden met de hand hebben bewerkt, kan een diagnostiekronde het hebben herschreven, of kan een formatter het hebben aangeraakt — alsnog doorvoeren zou dat werk weggooien en dan zou de diff opzettelijk lijken. Elke afwijking weigert de hele wijziging, want een gedeeltelijk toegepaste structurele herschrijving laat een boomstructuur achter die onder geen van beide vormen compileert.

Wat een debugger wel beantwoordt en een printinstructie niet

Sectie getiteld “Wat een debugger wel beantwoordt en een printinstructie niet”

De lus met printinstructies is: een print toevoegen, het hele ding opnieuw draaien, de uitvoer lezen, de print verwijderen. Die kost een volledige testrun per vraag, en beantwoordt alleen de vraag die je vóór het draaien bedacht had.

Een gestopt frame beantwoordt in één keer elke vraag over dat moment — elke local, elke aanroeper, en een expressie die in de eigen scope van het frame wordt geëvalueerd. Voor een vastgelopen proces of een fout die één op de tien runs optreedt, is dat het verschil tussen een middag en een minuut.

Begrensd als een omhulling, en om dezelfde reden

Sectie getiteld “Begrensd als een omhulling, en om dezelfde reden”

Een debugadapter bezit een gestopt proces dat vasthoudt wat dat proces ook vasthoudt — een poort, een lock, een databaseverbinding. Een sessie die niemand aanstuurt is een lek dat langer leeft dan de beurt die hem startte. Daarom is er een harde TTL, precies één sessie per gesprek, en wordt een tweede launch geweigerd in plaats van de eerste stil te vervangen: een agent die een tweede start zonder de eerste te beëindigen, is de eerste vrijwel zeker kwijtgeraakt.

Het Debug Adapter Protocol deelt de Content-Length-framing van het Language Server Protocol en niets daarboven — andere correlatiesleutels, een andere foutvorm, en een derde berichttype (gebeurtenissen) zonder LSP-equivalent. De framing staat op één plek; de protocollen zitten er apart bovenop.

De framing twee keer schrijven zou twee antwoorden betekenen op “een bericht beslaat twee chunks”, “een chunk bevat drie berichten” en “de server schreef een banner naar stdout vóór zijn eerste frame”, en slechts één daarvan zou de oplossing krijgen.