Suche/Einen JavaScript erzeugenden Compiler und sein Build-System ändern und jede Schicht testen

Einen JavaScript erzeugenden Compiler und sein Build-System ändern und jede Schicht testen

Für eine KI in einem Compiler-Monorepo, das JavaScript erzeugt. Sie baut über den Task-Runner, lässt eingefrorene Syntaxbaum-Dateien in Ruhe, testet jede geänderte Schicht, führt neue Build-Optionen ganz durch und misst das Tempo, statt dem Linter zu folgen.

Diese KI-Regeln verwenden
## Über den Task-Runner bauen, ohne eine Stufe zu überspringen Nutze den Task-Runner des Repositorys, statt die darunterliegenden Build-Werkzeuge von Hand aufzurufen. Das Standardziel baut die Plattform-Toolchain; separate Ziele fügen die Standardbibliothek hinzu, führen die Testsuiten aus, formatieren den Code und prüfen die Formatierung, ohne sie umzuschreiben. ### Die Ziele bauen aufeinander auf Die Installation der Abhängigkeiten läuft automatisch für jedes Ziel, das die JavaScript-Werkzeuge benötigt. Der Standard-Build erzeugt die Toolchain-Binaries – sowohl die Compiler-Programme als auch das separate Binary des Build-Systems – und kopiert sie in das plattformspezifische Paketverzeichnis. Das Ziel für die Standardbibliothek verwendet diese Toolchain-Ausgaben, um die Laufzeitquellen zu kompilieren. Die Testziele verwenden alles wieder, was über ihnen liegt. ### Überspringe keine Ebene Da jedes Ziel von dem Ziel darunter abhängt, kann der direkte Aufruf eines Werkzeugs auf niedrigerer Ebene veraltete Binaries zurücklassen und Ergebnisse liefern, die nicht zu einem sauberen Build passen. Wenn ein Build-Ergebnis unmöglich erscheint, führe das Clean-Ziel aus und baue von oben neu, bevor du weiter untersuchst. ## Die eingefrorene Syntaxbaum-Definition nicht ändern, sondern die Umwandlungsmodule anpassen Manche Dateien sind bewusst eingefroren, weil externe Parser-Erweiterungen von ihrer exakten Form abhängen. Ändere niemals die eingefrorene Fassung der Syntaxbaum-Definition. Wenn sich der aktive Syntaxbaum ändert, aktualisiere stattdessen die beiden Abbildungsmodule, die zwischen der aktiven und der eingefrorenen Darstellung umwandeln, damit Erweiterungen weiter funktionieren und sich der Hauptbaum frei weiterentwickeln kann. ### Kompatibilität mit dem ursprünglichen Fork ist keine Vorgabe Der Compiler entstand als Fork eines anderen Compilers, doch das Projekt pflegt seinen eigenen Syntaxbaum und darf brechende Änderungen gegenüber dem Original vornehmen. Entscheide danach, was diesem Kompilierungsziel am besten dient, nicht danach, was das Ursprungsprojekt tut. ### Neue Schlüsselwörter möglichst nicht reservieren Bevorzuge eine Umsetzung, die kein neues reserviertes Wort erfordert, denn eine solche Reservierung zerstört bestehenden Code, der den Namen bereits verwendet. Behandle neue Schlüsselwörter als letztes Mittel und suche zuerst nach einer Syntax, die bestehende Konstrukte wiederverwendet. ## Die Ursache beheben, statt Warnungen stummzuschalten oder Platzhalter-Wildcards zu nutzen Verwende niemals ein Attribut zur Warnungsunterdrückung, um den Compiler zum Schweigen zu bringen. Behebe stattdessen die zugrunde liegende Ursache – eine unterdrückte Warnung verbirgt genau die Art von Fehler, für deren Aufdeckung die Warnung existiert. ### Platzhaltermuster sind eine Entscheidung, keine Abkürzung Verwende Platzhaltermuster nicht als Lückenfüller für neue Sprachfunktionen, die dann vergessen werden. Setze eines nur ein, wenn du sicher bist, dass der Wert im konkreten Fall ignoriert werden soll, und stelle sicher, dass jede neue Sprachfunktion über alle Compiler-Schichten hinweg vollständig behandelt wird. ### Ergebnisse ausdrücklich verwerfen und Zustand weiterreichen Wird eine Funktion nur wegen ihrer Nebenwirkungen aufgerufen, nutze die ausdrückliche Ignorier-Funktion, statt das Ergebnis an ein Platzhaltermuster zu binden. Verwirf niemals ein zustandsbehaftetes Ergebnis – reiche es stattdessen durch die Aufrufkette weiter. ### Kleine Konventionen, die Muster ehrlich halten Hänge keinen abschließenden Unit-Parameter an eine Funktion, die bereits ein verpflichtendes benanntes Argument hat; das Label verhindert eine versehentliche partielle Anwendung schon von sich aus. Lass den abschließenden Platzhalter in Record-Mustern weg, wenn die zugehörige Warnung im Code-Bestand deaktiviert ist. Achte auf ähnlich benannte Konstruktoren in verschiedenen Zwischendarstellungen, denn derselbe Variantenname kann in verschiedenen Kompilierungsphasen Unterschiedliches bedeuten. ## Vor dem Ändern klären, welche Compiler-Schicht betroffen ist Die Pipeline verwandelt Quelltext im Parser in einen Oberflächen-Syntaxbaum, wendet Frontend-Transformationen an, erzeugt im Typprüfer einen typisierten Baum, senkt ihn auf eine Zwischendarstellung auf Lambda-Ebene ab, wandelt ihn in eine Zwischendarstellung auf JavaScript-Ebene um und gibt schließlich Code aus. Bestimme die Schicht, die du bearbeitest, bevor du irgendetwas anfasst. ### Die Schichten entsprechen Verzeichnissen Das Syntax-Verzeichnis ist für das Parsen und die Oberflächensyntax zuständig. Das Frontend-Verzeichnis ist für Syntaxbaum-Transformationen und die Verarbeitung der Fremdfunktionsschnittstelle zuständig. Das Verzeichnis des Typprüfers ist für die Typprüfung und Transformationen des typisierten Baums zuständig. Das Kernverzeichnis enthält sowohl die Module auf Lambda-Ebene als auch die auf JavaScript-Ebene, unterschieden durch ein Dateipräfix. Eigene Verzeichnisse enthalten erweiterte Hilfsfunktionen und Datenstrukturen, die Erzeugung von Typdefinitionen für das Zielökosystem sowie den Sprachserver und die Werkzeuge. ### Änderungen greifen meist über Schichten hinweg Eine neue Sprachfunktion erfordert typischerweise Arbeit am Parser, Aktualisierungen der Syntaxbaum-Definition, Unterstützung in der Typprüfung, Behandlung auf Lambda-Ebene, JavaScript-Erzeugung und Tests – in dieser Reihenfolge. Wird eine Schicht ausgelassen, entsteht eine Funktion, die sauber geparst wird und danach fehlerhaft kompiliert. ## Zum Debuggen die Zwischendarstellung jeder Stufe ausgeben Das Compiler-Binary akzeptiert Schalter, die jede Stufe ausgeben: den vorverarbeiteten Quelltext, den Parse-Baum nach dem Parsen, den typisierten Baum nach der Typprüfung und die nicht optimierte Lambda-Darstellung. Vergleiche die Ausgabe jeder Stufe mit deiner Erwartung, bevor du anfängst, den Quelltext des Compilers zu lesen. ### Ordne das Symptom der zuständigen Schicht zu Probleme bei der Ausgabeformatierung gehören zum Modul für die formatierte Ausgabe. Typfehler gehören zu den Modulen des Typprüfers. Falsche Ergebnisse, die erst nach der Optimierung auftreten, gehören zu den Analysedurchläufen auf Lambda-Ebene. Fehlerhaft ausgegebener Code gehört zu den Modulen der JavaScript-Erzeugung. ### Klein reproduzieren Kompiliere während des Ausprobierens eine einzelne minimale Datei statt des ganzen Projekts. Füge im Ausgabemodul für die Lambda-Darstellung vorübergehende Ausgaben hinzu, wenn eine Darstellung untersucht werden muss, und entferne diese Instrumentierung, bevor die Änderung übernommen wird. ## Jede geänderte Schicht sofort mit Tests abdecken Füge Tests hinzu, sobald du eine Compiler-Schicht änderst, und nicht erst, wenn alle Änderungen fertig sind. Spätes Testen verschleiert, welche Schicht eine Regression eingeführt hat, und macht aus einer kleinen Eingrenzung eine Untersuchung der gesamten Änderung. ### Fälle, die immer einen Test brauchen Neue Sprachfunktionen und Fehlerbehebungen erfordern immer Tests. Das Ändern eines Analysedurchlaufs oder der JavaScript-Erzeugung erfordert immer Tests. Prüfe immer die Qualität des ausgegebenen JavaScript, nicht nur dessen Korrektheit. ### Die richtigen Testsuiten abdecken Parser-Tests prüfen die Syntax. Integrationstests decken das Verhalten von Anfang bis Ende ab. Unit-Tests decken einzelne Compiler-Funktionen ab. Build-Tests decken Fehlerfälle und Randfälle ab, darunter Typfehler in einer einzelnen Datei sowie modulübergreifende Fehler, die getrennte Implementierungs- und Schnittstellendateien erfordern. Die Suiten für Hin- und Rückumwandlung, erzeugte Typen, Analyse und Werkzeuge haben jeweils ein eigenes Ziel und sollten ausgeführt werden, wenn sich der zugehörige Bereich ändert. ## Den Katalog der Fehler und Warnungen in derselben Änderung aktualisieren Das Repository führt je Modul einen Katalog aller Fehler und Warnungen, die der Compiler ausgeben kann; jeder Eintrag ist entweder einer Testvorlage zugeordnet oder einer dokumentierten Begründung, warum er nicht erreichbar ist. Der Katalog ist das wichtigste Werkzeug, um Abdeckungslücken und Kandidaten für die Entfernung toten Codes zu finden; veraltete Einträge erschweren daher beide Aufgaben. ### Den Katalog in derselben Änderung wie den Code aktualisieren Wenn du eine Fehlervariante hinzufügst oder entfernst, füge ihre Zeile im passenden Modulabschnitt hinzu oder entferne sie, setze ihren Status auf abgedeckt, nicht erreichbar oder noch zu erledigen und verlinke entweder die abdeckende Testvorlage oder halte fest, warum die Variante nicht erreicht werden kann. Wenn du eine Testvorlage hinzufügst oder entfernst, aktualisiere den Vorlagenpfad und den Status der betroffenen Zeile, damit Katalog und Testsuite niemals auseinanderlaufen. ## Schnell kompilieren und das erzeugte JavaScript lesbar halten Der Compiler ist auf schnelle Rückmeldeschleifen ausgelegt, muss auf große Code-Bestände skalieren, und seine erzeugte Ausgabe wird von Menschen gelesen. Beide Eigenschaften sind Prüfkriterien und keine Nebensache. ### Den ausgegebenen Code lesbar halten Vermeide die Ausgabe bedeutungsloser generierter Bezeichner und halte die Ausgabe lesbar. Eine Änderung, die korrekte, aber unlesbare Ausgabe erzeugt, ist nicht fertig. ### Vorher und nachher messen Berücksichtige bei jeder Änderung die Auswirkung auf die Kompiliergeschwindigkeit, setze die passenden Optimierungsdurchläufe auf Lambda- und JavaScript-Ebene ein und erstelle vor und nach jeder aus Leistungsgründen vorgenommenen Änderung ein Laufzeitprofil, statt sie allein am Code zu beurteilen. ## Namensregeln einhalten und sichtbare Änderungen ins Changelog eintragen Richte dich nach der Namenskonvention der Sprache, in der du gerade schreibst: Die Implementierungssprache des Compilers verwendet die Unterstrich-Schreibweise (snake case), während Quelldateien in der kompilierten Sprache selbst Binnenversalien (camel case) verwenden. Folge den vorhandenen Mustern im Code-Bestand, bevorzuge vorhandene Hilfsfunktionen, statt sie neu zu erfinden, kommentiere komplexe Algorithmen und nicht offensichtliche Logik und erhalte die Abwärtskompatibilität, wo dies sinnvoll ist. ### Commits bringen Pflichten mit sich Zeichne Commits in der vom Projekt geforderten Form ab, lege jeder Änderung die passenden Tests bei und stelle vor dem Committen sicher, dass der Build durchläuft. ### Nutzersichtbare Änderungen festhalten Ergänze für jede nutzersichtbare Änderung einen Eintrag im Änderungsprotokoll, sei es eine Fehlerbehebung, eine neue Funktion oder eine brechende Änderung. Setze ihn in den passenden Abschnitt der aktuellen, noch nicht veröffentlichten Version und beende die Zeile mit einem Link auf den Beitrag. Von eingereichten Beiträgen wird erwartet, dass sie einen solchen Eintrag enthalten. ## Eine neue Build-System-Option an jeder Stelle einbauen, die sie durchläuft Das begleitende Build-System, geschrieben in einer systemnahen Sprache, liefert schnelle inkrementelle Builds und bessere Fehlermeldungen. Seine Module sind sauber getrennt: zentrale Datenstrukturen wie der Build-Zustand und die Moduldatensätze, Kompilierung und Erzeugung der Compiler-Argumente, Erzeugung des Syntaxbaums und Parser-Argumente, Paketermittlung und Auflösung der Abhängigkeiten, der Modul-Abhängigkeitsgraph und die Zykluserkennung, das Aufräumen von Artefakten, Build-Protokollierung und Fehlermeldung, die Kommandozeilenschnittstelle, das Einlesen der Projektkonfiguration, die Dateiüberwachung und der Einstiegspunkt. ### Eine Option an jeder Stelle ergänzen, die sie durchläuft Deklariere den Schalter sowohl im Argumenttyp für den Build als auch in dem für die Überwachung und aktualisiere die Umwandlung zwischen beiden. Erweitere den Konfigurations-Parser um jedes neue Feld der Projektkonfiguration. Passe die betroffenen Build-Module an. Reiche den Parameter anschließend durch die gesamte Kette weiter: vom Einstiegspunkt in die Build-Funktion, in die Build-Initialisierung und in den Build-Zustand, und ebenso entlang des Überwachungspfads. Ergänze Unit-Tests für das neue Verhalten. ### Vorrang und Fehlerbehandlung vorab klären Kommandozeilen-Schalter haben Vorrang vor der Projektkonfigurationsdatei. Gib Fehler über den im Projekt üblichen Ergebnistyp mit Fehlerumhüllung weiter, statt das Programm abstürzen zu lassen, und nutze Protokollierung auf Debug-Ebene für Diagnosen während der Entwicklung. ## Das Build-System direkt starten und auf den lokalen Compiler zeigen lassen Führe die Toolchain-Befehle des Build-Systems aus dem Wurzelverzeichnis des Repositorys aus, indem du auf dessen Manifestdatei verweist – so wie es die kontinuierliche Integration tut –, statt in das Unterprojekt zu wechseln. Der Task-Runner bietet außerdem Abkürzungen, um es zu bauen und seine Integrationstests auszuführen. ### Das Entwicklungs-Binary auf die lokale Toolchain zeigen lassen Beim direkten Ausführen greift das Binary andernfalls auf eine installierte Veröffentlichung zurück. Exportiere vor dem Ausführen die Umgebungsvariablen für den Pfad des Compiler-Programms und für den Laufzeitpfad, beide aufgelöst als absolute Pfade innerhalb des aktuellen Arbeitsstands. Beachte, dass das Entwicklungs-Binary nach dem Paket statt nach dem Werkzeug benannt sein kann; prüfe daher den erzeugten Dateinamen, statt ihn anzunehmen. ### Die Ausführlichkeit erhöhen, wenn das Verhalten unklar ist Das Build-System akzeptiert Schalter für zunehmende Ausführlichkeit, für Protokollierung auf Info-Ebene und auf Debug-Ebene. So siehst du Einzelheiten wie etwa, welche Ordner eine Überwachungssitzung tatsächlich beobachtet. ### Die Ablaufverfolgung folgt der Standardspezifikation Die verteilte Ablaufverfolgung wird vollständig über die standardmäßigen Telemetrie-Umgebungsvariablen konfiguriert – Endpunkt des Sammlers, gesonderte Endpunkt-Überschreibung für die Ablaufverfolgung, Kopfzeilen des Exporteurs, Dienstname, Ressourcenattribute und Protokollierungsstufe – ohne projektspezifische Stellschrauben. Erst das Setzen eines Endpunkts aktiviert die Telemetrie; ist keiner gesetzt, bewirkt die Ablaufverfolgung nichts. Gib Ereignisse über die Makros der Ablaufverfolgung statt über die einfachen Protokollierungsmakros aus, wenn sie in den Ablaufverfolgungen erscheinen sollen, denn beide nutzen getrennte Systeme. ## CI-Tests auf eine Bedingung warten lassen statt auf eine feste Zeit Die Ausführungsumgebungen der kontinuierlichen Integration sind langsamer als Entwicklungsrechner, was feste Wartezeiten anfällig macht. Frage die Bedingung ab, auf die es dir wirklich ankommt – etwa das Erscheinen einer Datei –, statt für eine geschätzte Dauer zu warten. ### Asynchrones Herunterfahren ist noch kein Abschluss Eine Hilfsfunktion, die einer Überwachung das Beenden signalisiert – etwa durch Entfernen einer Sperrdatei –, sendet nur das Signal; sie wartet nicht darauf, dass der Prozess endet. Vermeide es, kurz vor dem Beenden Ereignisse durch Konfigurationsänderungen auszulösen, denn die Überwachung könnte einen nebenläufigen Neubau starten, mit dem du nicht gerechnet hast. ### Plattformspezifische Textänderungen nicht selbst zusammenbasteln Das Bearbeiten von Datenströmen an Ort und Stelle verhält sich je nach Betriebssystem unterschiedlich; verwende deshalb in Shell-Tests die Hilfsfunktionen des Repositorys für Ersetzung und Pfadnormalisierung statt roher Editoraufrufe. Ein einzelnes Integrationsskript lässt sich allein ausführen, indem du die Pfade für Programm und Laufzeit exportierst, die gemeinsame Hilfsdatei einbindest und genau dieses eine Skript aufrufst; das ist nützlich, um zu iterieren, ohne die gesamte Suite laufen zu lassen. ## Linter-Vorschläge an der echten Leistung messen Vorschläge des Linters sind Anregungen, keine Urteile. Wenn eine vorgeschlagene Umstrukturierung der Leistung oder der Verständlichkeit schaden könnte, beurteile den Zielkonflikt ausdrücklich, statt den Vorschlag mechanisch umzusetzen. ### Komplexität nur dort zulassen, wo sie sich auszahlt Eine Parameter-Struktur ist nicht automatisch besser als mehrere Argumente; erlaube die Linter-Regel lokal für Funktionen, die berechtigterweise viele Parameter brauchen und bei denen eine Struktur nur unnötigen Aufwand bedeuten würde. Eine Kopie ist annehmbar, wenn sie klein ist, einmalig erfolgt, von den Besitzregeln verlangt wird und außerhalb eines heißen Pfads liegt – konstruiere nicht mit übermäßigem Aufwand daran vorbei. ### Auf Basis von Belegen optimieren Erstelle vor dem Optimieren ein Laufzeitprofil. Die meisten Leistungsbedenken in einem Build-System sind neben der eigentlichen Kompilierzeit vernachlässigbar. Wo Effizienz doch zählt, halte die Parametertypen entlang einer Aufrufkette einheitlich, statt an jeder Grenze zwischen besitzenden und geliehenen Zeichenkettentypen umzuwandeln, denn diese Umwandlungen erzeugen zusätzliche Speicheranforderungen ohne jeden Nutzen. Kompiliere nur veränderte Module neu, parallelisiere unabhängige Arbeit, achte bei großen Projekten auf den Speicherbedarf des Build-Zustands und halte Dateisystemoperationen möglichst gering.