Blog 8 Min. Lesezeit

Bewertung der Front-End-Parser-Architektur mit CppDepend: Ein Deep Dive in EDG

Diesen Artikel teilen
Bewertung der Front-End-Parser-Architektur mit CppDepend: Ein Deep Dive in EDG

In diesem Beitrag untersuchen wir, warum die prozedurale C-Architektur von EDG auf monolithische Dispatcher und miteinander verknüpfte Header-Dateien setzt – und warum ein Refactoring dieser Strukturen Performance und Wartbarkeit tatsächlich verschlechtern würde.

Werfen wir einen Blick auf CppDepends Smart Code City für das EDG-Front-End:

CppDepend Smart Code City des EDG-Front-Ends mit Kuppeln für hohe Kopplung und monolithischen Dispatch-Säulen

Was die visuellen Muster im EDG-Front-End offenbaren

Betrachtet man die gerenderte Stadtübersicht von EDG, werden mehrere makro-architektonische Merkmale sofort sichtbar:

  • Kuppeln zeigen hohe Kopplung in den Kernbezirken: Man beachte die Cluster rot getönter Gebäude mit Kuppeln. In CppDepends 3D City markiert eine Kuppel gezielt eine Funktion mit hoher Kopplung – sie stützt sich also stark auf viele andere Teile der Codebasis oder interagiert mit ihnen. In EDG greifen Kernaufgaben wie Auswertung von Ausdrücken, Typauflösung und Deklarationsverarbeitung ständig auf globale Zustandstabellen und AST-Strukturen zu. Diese hohe gegenseitige Abhängigkeit (efferente Kopplung) lässt Kuppeln in allen primären Codebezirken auftauchen.
  • Monolithische Dispatch-Säulen vs. flache Nachbarschaften: Mehrere markante, hohe Monolithen mit breiter Grundfläche ragen über die umliegenden flachen Blöcke hinaus. Sie repräsentieren Funktionen wie process_expr_work: große prozedurale C-Switch-Schleifen mit Tausenden von Codezeilen und hoher zyklomatischer Komplexität.
  • Farbclusterung (Rot-/Orange-Dominanz in den zentralen Modulen): Während Hilfsmodule kühlere grün/türkise Oberflächen zeigen, weist der zentrale C++-Front-End-Bezirk (cpfe) durchgehend warme Farben auf. In Standardsoftware signalisiert weit verbreitetes Rot dringenden Refactoring-Bedarf; in einem industriellen C-Compiler beweist es visuell, wie eng die gesamte Parsing- und Auswertungspipeline integriert sein muss, um komplexe C++-Semantik effizient zu verarbeiten.
  • Grüne und blaue Begrenzungslinien zeigen starke Modulgesundheit: Man beachte, dass die Umrandungen der Hauptmodulzonen durchgehend grün und blau bleiben. Während einzelne Funktionen im Inneren hochgradig komplex sind, spiegeln CppDepends Begrenzungslinien die architektonische Gesundheit auf oberster Ebene, Regelkonformität und saubere Modulgrenzen wider.

Der monolithische Kern: process_expr_work

In CppDepends Smart Code City sticht die Funktion process_expr_work in src/interpret.c als eine der größten Strukturen der gesamten EDG-Codebasis hervor:

  • Lines of Code (LOC): 2.911
  • Zyklomatische Komplexität (CC): 1.229
  • Efferente Kopplung (EC): 506

Für ein klassisches statisches Analysewerkzeug lösen diese Kennzahlen sofortige Refactoring-Alarme mit höchster Priorität aus. Das Verständnis des prozeduralen C-Designs von EDG zeigt jedoch, warum diese Struktur sowohl beabsichtigt als auch effektiv ist.

Warum process_expr_work so lang ist: Die prozedurale C-Dispatch-Schleife

Da EDG historisch in prozeduralem C entwickelt wurde, verwendet es keine objektorientierten C++-Features wie Klassenvererbung oder virtuelle Funktionstabellen.

Stattdessen basieren die AST-Traversierung (Abstract Syntax Tree) und die Ausdrucksauswertung auf explizitem, tag-basiertem Dispatching:

  • Tag-basierte Diskriminatoren: AST-Knoten sind rohe C-Struct-Zeiger, die mit Enum-Kennungen für Ausdrucksarten versehen sind (z. B. EXPR_BINARY, EXPR_CAST, EXPR_CALL).
  • Zentralisierter prozeduraler Switch: process_expr_work fungiert als zentrale Interpreter-Dispatch-Schleife. Sie führt eine einzige, massive C-switch-Anweisung aus, die über Hunderte von AST-Knotenarten verzweigt.
  • Lokale Registerallokation & null Call-Overhead: Die Auswertungsschritte innerhalb eines einzigen Funktionsscopes zu halten, vermeidet das Aufsetzen von Stack-Frames und indirekte Funktionsaufrufe während rekursiver AST-Traversierungen. Zudem können Standard-C-Compiler die Registerallokation über lokale temporäre Variablen hinweg optimieren.

Wie Clang dasselbe Verhalten implementiert

Clang parst und wertet Ausdrücke für exakt dieselbe C++-Sprachspezifikation aus, verfolgt jedoch einen objektorientierten C++-Ansatz. Der Vergleich zwischen EDGs prozeduralem C-Dispatch und Clang zeigt, wie Sprachparadigmen die Signatur der statischen Analyse verändern, während die zugrunde liegenden architektonischen Anforderungen erhalten bleiben:

Metrik / MerkmalEDG (process_expr_work)Clang (ExprConstant.cpp)
SprachparadigmaProzedurales CModernes C++ (objektorientiert)
ArchitekturmusterZentralisierte Tagged-switch-Dispatch-SchleifeAST-Visitor-Pattern (ConstStmtVisitor)
Code-StrukturEinzelne monolithische Funktion (2.900+ LOC)Verteilt über eine Klassenhierarchie (VisitExpr, VisitCast usw.)
Signatur der statischen AnalyseVertikale Komplexität: hohe CC (1.229) in einer FunktionHorizontale Komplexität: geringere CC pro Methode, aber höhere Klassenzahl
Call Stack & SpeicherGeringer Stack-Overhead; optimierte RegisterwiederverwendungVirtuelle/überladene Methodenaufrufe über Klassenhierarchien
KontextübergabeDirekter Zugriff auf lokalen Funktionsscope & ZustandExplizite Kontextobjekte (EvalInfo&) werden an Methoden übergeben

Wohin die Komplexität wandert

In EDG: Die Komplexität ist vertikal und lokalisiert. Sie sammelt sich in process_expr_work und erzeugt eine hohe zyklomatische Komplexität (1.229) in einer einzigen Übersetzungseinheit.

In Clang: Die Komplexität ist horizontal und verteilt. Clang teilt die Auswertungslogik auf einzelne Visit*-Methoden in mehreren Evaluator-Klassen auf (IntExprEvaluator, FloatExprEvaluator, LValueExprEvaluator).

Clang vermeidet zwar 3.000 Zeilen lange Einzelfunktionen, doch die gesamte Systemkomplexität bleibt identisch, da die Auswertung von C++-Ausdrücken – mit allen impliziten Konvertierungen, Operatorüberladungen, Template-Instanziierungen und Dialekt-Sonderfällen – exakt denselben Regelsatz berücksichtigen muss.

Ob als 2.900 Zeilen langer prozeduraler C-Switch in EDG oder als 50 Visitor-Methoden über eine Klassenhierarchie in Clang – die zugrunde liegende Domänenkomplexität bleibt unverändert.

Abhängigkeitszyklen auf Dateiebene: Die Matrix (DSM) lesen

Der Wechsel von der Smart Code City zu CppDepends Dependency Structure Matrix (DSM) offenbart eine weitere wichtige Signatur der statischen Analyse: dichte Abhängigkeitszyklen auf Dateiebene zwischen den zentralen Übersetzungseinheiten.

Dependency-Structure-Matrix der EDG-Quelldateien mit Zyklen zwischen class_decl.c, declarator.c, decls.c und expr.c

Betrachtet man das DSM-Raster, bilden Dateien wie src/class_decl.c, src/declarator.c, src/decls.c und src/expr.c eine markante stark zusammenhängende Komponente (Strongly Connected Component, SCC):

  • Blaue Zellen: Kennzeichnen unidirektionale, geschichtete Abhängigkeiten (Datei A hängt von Datei B ab, aber nicht umgekehrt).
  • Rote Zellen: Markieren bidirektionale Abhängigkeiten (Zyklen), bei denen zwei Dateien direkt oder indirekt auf die Symbole und Definitionen der jeweils anderen angewiesen sind.
  • Zellenzahlen: Zeigen das exakte Kopplungsgewicht der Member (z. B. verwendet declarator.c 31 Member aus decls.h und 5 Member aus declarator.h).

Die gegenseitige Abhängigkeit von class_decl.c && declarator.c

In der traditionellen Enterprise-C/C++-Architektur gelten zirkuläre Abhängigkeiten zwischen Übersetzungseinheiten als schwerwiegender Designfehler.

Den Zyklus sezieren: Pragmatische gegenseitige Rekursion vs. zufällige Kopplung

Mit Code Quest in CppDepend können wir die präzisen Funktionsaufrufe untersuchen, die den bidirektionalen Zyklus zwischen src/class_decl.c und src/declarator.c antreiben.

Indem wir Methoden in class_decl.c abfragen, die von declarator.c verwendet werden – und umgekehrt –, decken wir zwei unterschiedliche Kopplungskategorien auf: pragmatische Zyklen, die fundamentale Sprachregeln widerspiegeln, und zufällige Zyklen, die sich leicht refaktorisieren lassen.

Funktionen aus declarator.c, die von class_decl.c verwendet werden:

Code-Quest-Abfrage der fünf declarator.c-Methoden, die von class_decl.c verwendet werden

Funktionen aus class_decl.c, die von declarator.c verwendet werden:

Code-Quest-Abfrage der sechs class_decl.c-Methoden, die von declarator.c verwendet werden

Die Absicht hinter dem Abhängigkeitszyklus class_decl.c ↔ declarator.c

1. Pragmatische gegenseitige Rekursion
(Als bewusstes Design beibehalten)

  • declarator()
  • scan_lambda_declarator()
  • abstract_class_diagnostic()

2. Zufällige Kopplung / Refactoring-Kandidaten
(Kandidaten für Entkopplung)

  • f_consume_any_stray_microsoft_rparen()
  • in_cli_property_or_event_definition()
  • in_static_cli_property_or_event_definition()

1. Die pragmatische Seite: Sprachinhärente gegenseitige Rekursion

Beim Parsen von modernem C++ sind Klassendefinitionen und Declarator-Parsing inhärent miteinander verknüpft. Die Code-Quest-Abfragen bringen Kernfunktionen an die Oberfläche, die die Abhängigkeit rechtfertigen:

A. declarator.c → class_decl.c

abstract_class_diagnostic(...): Wird von declarator.c aufgerufen, wenn eine Funktions- oder Variablendeklaration versucht, eine abstrakte Klasse zu instanziieren oder zurückzugeben. Der Declarator-Parser muss Klassenlayout-Metadaten aus class_decl.c abfragen, um rein virtuelle Funktionen auszuwerten und Diagnosen auszugeben.

B. class_decl.c → declarator.c

declarator(...) & scan_lambda_declarator(...): Werden von class_decl.c aufgerufen, sobald eine Klassendefinition auf Member-Funktions-Deklaratoren, Zeiger auf Member oder verschachtelte Lambdas trifft.

delayed_scan_of_exception_spec(...): Exception-Spezifikationen an Member-Funktionen können erst nach dem Parsen der relevanten Klassenkontexte verarbeitet werden.

Der Versuch, diese Kernaufrufe durch übermäßige Abstraktion zu eliminieren, würde künstliche Wrapper-Schichten und spürbare Performance-Einbußen zur Laufzeit einführen. In der Compiler-Technik ist diese gegenseitige Rekursion pragmatisches, domänengetriebenes Design.

2. Die Refactoring-Chancen: Zufällige Utility-Kopplung

Code Quest isoliert jedoch auch Funktionen im Zyklus, die wenig mit der zentralen C++-Parsing-Grammatik zu tun haben und eine zufällige architektonische Kopplung darstellen:

Code-Quest-Abfrage der sechs class_decl.c-Methoden, die von declarator.c verwendet werden

Betrachtet man die 6 zurückgegebenen Methoden in class_decl.c:

  • Dialekt-/Parser-Helfer: f_consume_any_stray_microsoft_rparen()
  • CLI-/Erweiterungs-Helfer: in_cli_property_or_event_definition() und in_static_cli_property_or_event_definition()

Warum diese unnötige Zyklen erzeugen

f_consume_any_stray_microsoft_rparen() ist ein Parser-Recovery-Hilfsmittel für MSVC-spezifische Syntax-Eigenheiten. Es in class_decl.c zu platzieren, zwingt declarator.c dazu, von der gesamten Klassendeklarations-Einheit abzuhängen – nur um ein einzelnes Token zu konsumieren!

Wie man den zufälligen Zyklus entfernt

  • Utility-Helfer extrahieren: Das Verschieben der Dialekt-Recovery-Routinen (f_consume_...) und CLI-Zustandsprüfungen in ein dediziertes Utility-Modul (z. B. src/parser_utils.c oder src/lex_helpers.c) durchbricht die künstliche Verbindung.
  • Ergebnis: Die Kopplungsfläche der 6 Methoden schrumpft, der zufällige Zyklus wird eliminiert – während die notwendigen, hochperformanten C++-Grammatik-Dispatch-Schleifen erhalten bleiben.

Zentrale Erkenntnis für Architekten

Warnungen statischer Analysewerkzeuge sollten niemals pauschal nach der Regel „alle Zyklen beheben“ behandelt werden.

Mit Werkzeugen wie CppDepend und Code Quest können Teams zwischen domänennotwendigen Zyklen (wie declarator() ↔ abstract_class_diagnostic()), die in eine C++-Parser-Engine gehören, und zufällig platzierten Utilities (wie MSVC-Token-Recovery-Helfern) unterscheiden, die sauber herausrefaktorisiert werden können.

Da C++ Inline-Klassendefinitionen, Member-Funktionen, verschachtelte Klassen und nachgestellte Rückgabetypen erlaubt, sind das Parsen von Klassendefinitionen und das Declarator-Parsing fundamental gegenseitig rekursiv.

Warum C-Header-Mechaniken die Zyklen verschärfen

Da EDG in prozeduralem C umgesetzt ist, stützt es sich auf gemeinsam genutzte Header-Definitionen (class_decl.h, declarator.h, decls.h).

  • Gemeinsame C-Typen: Datenstrukturen wie a_type_ptr, a_decl_ptr und a_declarator müssen in beiden Übersetzungseinheiten sichtbar sein.
  • Kreuz-Inklusion & Vorwärtsdeklarationen: Während sauberes C++ Interfaces oder Pimpl-Muster zur Entkopplung von Headern nutzen könnte, verlassen sich C-Codebasen auf sich gegenseitig inkludierende Header und vorwärtsdeklarierte rohe Struct-Zeiger. In der DSM zeigt sich das als dichte rote Cluster über class_decl.c, declarator.c und decls.c.

Zentrale Erkenntnis für die Matrix-Analyse

In Standard-Softwarearchitekturen bedeutet ein rotes Quadrat in einer DSM: „Brich dies mit Interface-Abstraktion auf.“

In einem industriellen Compiler-Front-End spiegelt ein roter Matrix-Cluster zwischen class_decl, declarator, decls und expr schlicht die Tatsache wider, dass sich die C++-Grammatikregeln nicht sauber in einen reinen gerichteten azyklischen Graphen (DAG) schichten lassen. Die Zyklen sind kein zufälliger architektonischer Verfall – sie spiegeln die gegenseitige Rekursion wider, die in die C++-Sprachspezifikation selbst eingebaut ist.

Das Qualitäts-Dashboard: 23.000+ Low-Severity-Issues entschlüsseln

Bei der Betrachtung von CppDepends übergeordnetem Qualitäts-Dashboard für EDG zeigt sich ein auffälliges statistisches Paradoxon:

CppDepend-Qualitäts-Dashboard für EDG mit 372.391 Codezeilen, 23.985 Issues und 5 von 5 bestandenen Quality Gates

Das EDG-Qualitäts-Dashboard auf einen Blick

Metrik / KategorieWert / Details
Gesamte Codezeilen (LOC)372.391
Issues gesamt23.985
Low-Severity-Issues22.620 (~94 % des Gesamtwerts)
High-Severity-Issues1.344
Verletzte kritische Regeln0
Quality-Gate-Status5/5 BESTANDEN

Auf den ersten Blick mag die Zahl von 23.985 Issues alarmierend wirken. Die Aufschlüsselung zeigt jedoch, warum EDG eine Gesamtbewertung von Rating B erreicht und alle 5 Quality Gates sauber besteht – ohne einen einzigen Fail- oder Warn-Status auszulösen.

1. Schweregrad-Aufschlüsselung: Rauschen vs. kritisches Risiko

Das Dashboard klassifiziert Issues in verschiedene Schweregrade:

  • Kritische Issues: 0 (keine schweren Speicherlecks, kein undefiniertes Verhalten, keine systemgefährdenden Fehler).
  • High-Severity-Issues: 1.344 (hauptsächlich konzentriert in Funktionen mit hoher zyklomatischer Komplexität wie process_expr_work).
  • Medium-Severity-Issues: 21
  • Low-Severity-Issues: 22.620 (~94 % aller gemeldeten Issues)

Der überwiegende Teil der Gesamtzahl stammt aus Low-Severity-Code-Style-Richtlinien, etwa den statischen MISRA-C/C++-Prüfungen.

2. Der MISRA-C-Effekt: Formatierungsregeln skalieren mit den LOC

Da EDG in prozeduralem C geschrieben ist, erzwingen statische Analyseregeln wie MISRA C strikte syntaktische Richtlinien über alle 372.391 Codezeilen hinweg. Ein klassisches Beispiel:

„Auf ein if(condition)-Konstrukt soll eine zusammengesetzte Anweisung (geschweifte Klammern {}) folgen.“

In klassischem prozeduralem C sind einzeilige if-Anweisungen ohne explizite Klammern üblich:

// Löst bei jedem Vorkommen eine Low-Severity-MISRA-C-Style-Verletzung aus
if (expr == NULL) return;

// MISRA-C-konforme Form
if (expr == NULL) {
    return;
}

Wenn eine Codebasis mit über 370.000 LOC optionale Klammern weglässt oder Makro-Stilmuster in Tausenden von switch-Fällen verwendet, markiert CppDepend jedes einzelne Vorkommen als Issue.

Obwohl diese Regeln Zehntausende einzelner Warnungen erzeugen, ist ihr Beitrag zur technischen Schuld pro Instanz minimal – deshalb zeigt Code Implementation 33.280 Issues, während Code Safety 0 Issues zeigt.

3. Hohe Kommentardichte spiegelt starke Dokumentation wider

Eine weitere herausragende Kennzahl auf dem Dashboard ist die Kommentarrate:

  • Kommentaranteil: 48,44 %
  • Kommentarzeilen: 349.808

Nahezu 50 % der gesamten EDG-Codebasis bestehen aus Kommentaren. Auf jede Zeile ausführbaren prozeduralen C-Codes kommt nahezu eine volle Zeile Dokumentation, die Eigenheiten der C++-Standardkonformität, Compiler-Flags und Dialekt-Sonderfälle erklärt.

Diese außergewöhnliche Kommentardichte erklärt, warum die Codebasis trotz struktureller Komplexität in den AST-Traversierungs-Dispatchern für ihre Domäne hervorragend wartbar bleibt.

Zentrale Erkenntnis

Das Qualitäts-Dashboard zeigt, warum rohe Issue-Zahlen ohne Schweregrad-Klassifizierung irreführend sein können.

EDGs 22.620 Low-Severity-Issues sind kosmetische Stil- und Formatierungsverletzungen (wie MISRA-Klammerkonformität). Da die Engine null Verletzungen kritischer Regeln und eine Kommentardichte von 48 % aufweist, erfüllt sie CppDepends oberste Quality Gates – bei gleichzeitig industrietauglichem C++-Parsing.

Fazit: Statische Metriken mit Domänenarchitektur in Einklang bringen

Die Analyse einer ausgereiften, industrietauglichen Codebasis wie dem EDG-C/C++-Front-End mit CppDepend bietet eine wertvolle Lektion in Softwarearchitektur: Statische Analysemetriken sind diagnostische Indikatoren, keine absoluten Dogmen.

Lektionen aus der EDG-Analyse

Zentrale ErkenntnisArchitektonische Einsicht
1. Kontext vor Metrik-DogmaEine 2.900 Zeilen lange Funktion mit CC 1.229 ist nicht immer technische Schuld – in einem C-basierten AST-Interpreter vermeidet sie Call-Stack-Allokation und Register-Overhead.
2. Zyklustypen unterscheidenGegenseitige Rekursion beim Grammatik-Parsing (declarator ↔ class_decl) ist pragmatisch, während verstreute MSVC-Token-Helfer behebbare technische Schuld sind.
3. Makro- vs. Mikro-GesundheitTrotz hoher lokaler Funktionskomplexität belegen grüne Begrenzungslinien und eine Kommentardichte von 48 % diszipliniertes Engineering auf Systemebene.

Zentrale Engineering-Erkenntnisse

  • Hohe Komplexität kann bewusstes Design sein: Funktionen wie process_expr_work erreichen astronomische Werte bei der zyklomatischen Komplexität (1.229), weil sie die Auswertung von C++-AST-Ausdrücken in hochdurchsatzfähigen prozeduralen C-Dispatch-Schleifen zentralisieren. Die Aufspaltung jedes Zweigs in separate Funktionen würde Parameter-Drilling, Stack-Frame-Overhead und Cache-Misses erhöhen, ohne die inhärente Komplexität der C++-Sprachspezifikation zu reduzieren.
  • Nicht alle Abhängigkeitszyklen sind gleich: Durch DSM-Visualisierung und Code-Quest-Abfragen haben wir gesehen, dass Abhängigkeitszyklen auf Dateiebene oft eine doppelte Natur haben:
    • Domänengetriebene Zyklen: Die gegenseitige Rekursion zwischen class_decl.c und declarator.c spiegelt direkt die rekursiven Regeln der C++-Grammatik wider (Klassendefinitionen enthalten Declaratoren; Declaratoren benötigen Klassenkontext).
    • Zufällige Zyklen: Das Platzieren von Token-Parser-Helfern (wie f_consume_any_stray_microsoft_rparen()) in zentralen Klassenmodulen erzeugt unnötige dateiübergreifende Kopplung, die sauber refaktorisiert werden kann.
  • Schweregrad-Klassifizierung verhindert Metrik-Panik: EDGs Qualitäts-Dashboard verzeichnet knapp 24.000 Issues. Doch mit 0 verletzten kritischen Regeln, 5/5 bestandenen Quality Gates und ~94 % Low-Severity-Warnungen aus strengen Style-Prüfungen (wie MISRA-C-Klammerkonformität) erreicht das Projekt ein solides Rating B. Zusammen mit einer Kommentardichte von 48,44 % demonstriert die Codebasis außergewöhnliche Wartungshygiene – trotz ihrer rohen strukturellen Größe.

Das letzte Wort für Softwarearchitekten

Statische Analysewerkzeuge wie CppDepend sind von unschätzbarem Wert, um strukturelle Heatmaps, architektonische Grenzen und verborgene Kopplung sichtbar zu machen. Das Ziel der statischen Analyse in komplexen Engineering-Domänen ist jedoch nicht, um jeden Preis alle Metriken auf Grün zu zwingen.

Durch die Kombination automatisierter Visualisierung mit einem tiefen Verständnis der Domänenanforderungen – ob beim Bau eines Compiler-Front-Ends, einer Game-Engine oder eines Echtzeit-Betriebssystems – können Architekten zwischen echter, wachsender technischer Schuld und pragmatischen, hochperformanten Designentscheidungen unterscheiden.

Diesen Artikel teilen