====== Lyx Compiler — KI-Integration (WP-KI-01 / WP-KI-02) ======
Zwei neue Compiler-Flags ermöglichen maschinenlesbare JSON-Ausgabe für KI-Agenten, Language-Server, IDEs und Build-Tooling: ''%%--%%ast-json'' exportiert den vollständigen Parser-AST, ''%%--%%error-json'' liefert strukturierte Fehler-Records statt Freitext.
→ [[lyx_-_programmiersprache:tools|Tools & Compiler]] · [[lyx_-_programmiersprache:tools:compiler-parameter|Compiler-Parameter]]
----
===== Flags im Überblick =====
^ Flag ^ Zweck ^
| ''%%--%%ast-json'' | Parser-AST als JSON-Array auf stdout ausgeben, dann beenden |
| ''%%--%%ast-json-pretty'' | Wie ''%%--%%ast-json'', zusätzlich 2-Leerzeichen-Einrückung |
| ''%%--%%error-json'' | Compiler-Fehler (Sema) als strukturiertes JSON statt Freitext; Exit-Code 1 bei Fehler |
**Reihenfolge im Compile-Ablauf:**
* ''%%--%%ast-json'' / ''%%--%%ast-json-pretty'': endet **nach dem Parsen**, vor der Semantic Analysis. Sema und Codegen laufen nicht.
* ''%%--%%error-json'': läuft durch alle Passes; bei Sema-Fehler wird JSON ausgegeben und mit Exit 1 beendet. Bei Erfolg kein JSON-Output.
----
===== --ast-json / --ast-json-pretty =====
Implementiert in ''src/backend/ast_json.lyx'' (Klasse ''AstJson'').
==== Schnellstart ====
# Kompakter AST eines Lyx-Programms
lyxc main.lyx --ast-json > ast.json
# Lesbar formatiert (2-Leerzeichen-Einrückung)
lyxc main.lyx --ast-json-pretty > ast.json
# AST pipen und mit jq auswerten
lyxc main.lyx --ast-json | jq '.[].kind'
lyxc main.lyx --ast-json | jq '[.[] | select(.kind=="NK_FUNC_DECL") | .sval]'
==== JSON-Struktur ====
Die Ausgabe ist ein JSON-Array der Top-Level-Knoten (Deklarationen). Jeder Knoten ist ein Objekt mit folgenden Feldern:
^ Feld ^ Typ ^ Pflicht ^ Bedeutung ^
| ''kind'' | string | ✔ | Symbolischer Knotentyp (z.B. ''NK_FUNC_DECL'') |
| ''kindId'' | integer | ✔ | Numerische Knoten-ID |
| ''line'' | integer | ✔ | Quellzeile (1-basiert; 0 = unbekannt) |
| ''col'' | integer | ✔ | Spalte ab Zeilenbeginn (0-basiert; 0 = unbekannt) |
| ''sval'' | string | nur wenn vorhanden | String-Wert (Bezeichner, String-Literal, Typname …) |
| ''ival'' | integer | nur wenn ≠ 0 | Integer-Wert (Literal, Operatorkode …) |
| ''pub'' | true | nur wenn vorhanden | Knoten ist mit ''pub'' deklariert |
| ''c0'' | array oder null | ✔ | Kinder-Sibling-Kette (Slot 0) |
| ''c1'' | array oder null | ✔ | Kinder-Sibling-Kette (Slot 1) |
| ''c2'' | array oder null | ✔ | Kinder-Sibling-Kette (Slot 2) |
| ''c3'' | array oder null | ✔ | Kinder-Sibling-Kette (Slot 3) — Sonderfall: siehe unten |
| ''libSpan'' | integer | Sonderfall | Nur bei ''NK_FUNC_DECL'' mit ''c2 < 0'' (extern fn): Library-Span statt ''c3''-Array |
**Sonderfall extern fn:** Wenn ein ''NK_FUNC_DECL''-Knoten keinen Body hat (extern-Deklaration), ist ''c2 = -1'' und das Feld ''c3'' entfällt. Stattdessen erscheint ''libSpan'' mit dem Integer-Wert des Library-Identifiers.
==== Beispiel-Ausgabe ====
Für ''pub fn Add(a: int64, b: int64): int64 { return a + b; }'':
[
{
"kind": "NK_FUNC_DECL",
"kindId": 2,
"line": 1,
"col": 7,
"sval": "Add",
"pub": true,
"c0": [
{"kind": "NK_PARAM", "kindId": 3, "line": 1, "col": 11, "sval": "a",
"c0": [{"kind": "NK_TYPE_NAME", "kindId": 60, "line": 1, "col": 14, "sval": "int64",
"c0": null, "c1": null, "c2": null, "c3": null}],
"c1": null, "c2": null, "c3": null},
{"kind": "NK_PARAM", "kindId": 3, "line": 1, "col": 23, "sval": "b",
"c0": [{"kind": "NK_TYPE_NAME", "kindId": 60, "line": 1, "col": 26, "sval": "int64",
"c0": null, "c1": null, "c2": null, "c3": null}],
"c1": null, "c2": null, "c3": null}
],
"c1": [{"kind": "NK_TYPE_NAME", "kindId": 60, "line": 1, "col": 40, "sval": "int64",
"c0": null, "c1": null, "c2": null, "c3": null}],
"c2": null,
"c3": [
{"kind": "NK_BLOCK", "kindId": 20, "line": 1, "col": 47,
"c0": [{"kind": "NK_RETURN", "kindId": 23, ...}],
"c1": null, "c2": null, "c3": null}
]
}
]
==== NK_*-Knotentypen ====
Vollständige Tabelle aller 116 Parser-Knotentypen:
**Deklarationen**
^ ID ^ Name ^ Bedeutung ^
| 0 | ''NK_INVALID'' | Ungültiger Knoten (Initialisierungswert) |
| 1 | ''NK_IMPORT'' | ''import''-Anweisung |
| 2 | ''NK_FUNC_DECL'' | Funktionsdeklaration (''pub fn'', ''fn'', extern fn) |
| 3 | ''NK_PARAM'' | Funktionsparameter |
| 4 | ''NK_VAR_DECL'' | ''var''-Deklaration |
| 5 | ''NK_CON_DECL'' | ''con''-Konstante |
| 6 | ''NK_STRUCT_DECL'' | ''struct''-Deklaration |
| 7 | ''NK_FIELD_DECL'' | Strukturfeld |
| 8 | ''NK_ENUM_DECL'' | ''enum''-Deklaration |
| 9 | ''NK_ENUM_MEM'' | Enum-Member |
| 10 | ''NK_TYPE_DECL'' | ''type''-Alias-Deklaration |
| 63 | ''NK_GENERIC_DECL'' | Generische Typ-Deklaration |
| 69 | ''NK_CLASS_DECL'' | ''class''-Deklaration |
| 70 | ''NK_IFACE_DECL'' | ''interface''-Deklaration |
| 90 | ''NK_DIM_DECL'' | ''dim''-Deklaration |
| 91 | ''NK_UTYPE_DECL'' | Unit-Typ-Deklaration |
| 110 | ''NK_CAPABILITY_ATTR'' | ''@capability''-Attribut |
| 111 | ''NK_CAPABILITY_DECL'' | Capability-Deklaration |
| 112 | ''NK_CAPABILITY_ARG'' | Capability-Argument |
| 113 | ''NK_NETWORK_TARGET'' | Netzwerk-Target-Deklaration |
| 114 | ''NK_GRANT_CLAUSE'' | ''grant''-Klausel |
| 115 | ''NK_RESTRICT_CLAUSE'' | ''restrict''-Klausel |
| 116 | ''NK_USES_CALLER_CAP'' | ''uses_caller_cap''-Annotation |
**Statements**
^ ID ^ Name ^ Bedeutung ^
| 20 | ''NK_BLOCK'' | Block ''{ … }'' |
| 21 | ''NK_IF'' | ''if''-Statement |
| 22 | ''NK_WHILE'' | ''while''-Schleife |
| 23 | ''NK_RETURN'' | ''return''-Statement |
| 24 | ''NK_EXPR_STMT'' | Ausdruck als Statement |
| 25 | ''NK_ASSIGN'' | Zuweisung '':='' |
| 26 | ''NK_FOR'' | ''for''-Schleife (foreach) |
| 27 | ''NK_SWITCH'' | ''switch''-Statement |
| 28 | ''NK_CASE'' | ''case''-Zweig |
| 29 | ''NK_BREAK'' | ''break'' |
| 30 | ''NK_DISPOSE'' | ''dispose''-Statement |
| 31 | ''NK_TRY'' | ''try''-Block |
| 32 | ''NK_CATCH'' | ''catch''-Block |
| 33 | ''NK_FINALLY'' | ''finally''-Block |
| 34 | ''NK_THROW'' | ''throw''-Statement |
| 35 | ''NK_CONTINUE'' | ''continue'' |
| 36 | ''NK_ASSERT'' | ''assert''-Statement |
| 77 | ''NK_REPEAT_UNTIL'' | ''repeat … until''-Schleife |
| 78 | ''NK_TUPLE_VAR_DECL'' | Tuple-Destrukturierung |
| 102 | ''NK_FOR_C'' | C-Stil-''for''-Schleife |
| 106 | ''NK_DEFER'' | ''defer''-Statement |
| 107 | ''NK_FOR_RANGE'' | ''for .. in range''-Schleife |
**Ausdrücke**
^ ID ^ Name ^ Bedeutung ^
| 40 | ''NK_LIT_INT'' | Integer-Literal |
| 41 | ''NK_LIT_FLOAT'' | Float-Literal |
| 42 | ''NK_LIT_STR'' | String-Literal |
| 43 | ''NK_LIT_CHAR'' | Char-Literal |
| 44 | ''NK_LIT_BOOL'' | ''true'' / ''false'' |
| 45 | ''NK_LIT_NULL'' | ''null'' |
| 46 | ''NK_IDENT'' | Bezeichner |
| 47 | ''NK_BINOP'' | Binärer Operator; ''ival'' enthält Operator-Code |
| 48 | ''NK_UNOP'' | Unärer Operator |
| 49 | ''NK_CALL'' | Funktionsaufruf |
| 50 | ''NK_INDEX'' | Array-Index ''a[i]'' |
| 51 | ''NK_FIELD'' | Feldzugriff ''a.b'' |
| 52 | ''NK_NEW'' | ''new''-Ausdruck |
| 53 | ''NK_CAST'' | ''as''-Cast |
| 54 | ''NK_CLOSURE'' | Closure / Lambda |
| 55 | ''NK_FN_PTR'' | Funktionszeiger |
| 71 | ''NK_SUPER_CALL'' | ''super.method()''-Aufruf |
| 72 | ''NK_IS_EXPR'' | ''is''-Typprüfung |
| 73 | ''NK_RANGE'' | Bereichsausdruck ''a..b'' |
| 79 | ''NK_MAP_LIT'' | Map-Literal |
| 80 | ''NK_SET_LIT'' | Mengen-Literal |
| 81 | ''NK_IN_EXPR'' | ''in''-Ausdruck |
| 82 | ''NK_MAP_ENTRY'' | Map-Eintrag (Schlüssel:Wert) |
| 83 | ''NK_POOL'' | Memory-Pool-Ausdruck |
| 84 | ''NK_PANIC'' | ''panic''-Ausdruck |
| 86 | ''NK_FORMAT_EXPR'' | Formatierter String-Ausdruck |
| 87 | ''NK_NULL_COAL'' | Null-Coalescing ''??'' |
| 88 | ''NK_SAFE_CAST'' | Sicherer Cast mit Fallback |
| 89 | ''NK_REGEX_LIT'' | Regulärer-Ausdruck-Literal |
| 94 | ''NK_SIMD_NEW'' | SIMD-Array-Allokation |
| 100 | ''NK_ARRAY_LIT'' | Array-Literal |
| 108 | ''NK_NAMED_ARG'' | Benanntes Argument ''key: val'' |
| 109 | ''NK_TYPE_TUPLE'' | Tuple-Typ |
**Typen**
^ ID ^ Name ^ Bedeutung ^
| 60 | ''NK_TYPE_NAME'' | Einfacher Typname (''int64'', ''bool'', benutzerdefinierter Typ) |
| 61 | ''NK_TYPE_ARRAY'' | Array-Typ ''array'' |
| 62 | ''NK_TYPE_PARAM'' | Typ-Parameter (Generics) |
| 92 | ''NK_TYPE_RINGBUF'' | Ringpuffer-Typ |
| 93 | ''NK_TYPE_PARALLEL'' | Parallel-Typ |
| 101 | ''NK_TYPE_ARRAY_FIXED'' | Fest dimensioniertes Array ''[N]T'' |
**Pattern-Matching**
^ ID ^ Name ^ Bedeutung ^
| 64 | ''NK_MATCH'' | ''match''-Ausdruck |
| 65 | ''NK_MATCH_CASE'' | ''case''-Zweig im ''match'' |
| 66 | ''NK_PATTERN_LIT'' | Literal-Pattern |
| 67 | ''NK_PATTERN_WILD'' | Wildcard-Pattern ''_'' |
| 68 | ''NK_PATTERN_BIND'' | Bind-Pattern (Variable binden) |
| 74 | ''NK_WHERE'' | ''where''-Klausel |
| 75 | ''NK_PATTERN_ENUM'' | Enum-Pattern |
| 76 | ''NK_PATTERN_STRUCT'' | Struct-Destrukturierung |
| 85 | ''NK_PATTERN_OR'' | Oder-Pattern ''A | B'' |
**LFD (Qt-UI-Builder)**
^ ID ^ Name ^ Bedeutung ^
| 95 | ''NK_LFD_FORM'' | LFD-Form-Wurzel |
| 96 | ''NK_LFD_WIDGET'' | Widget-Deklaration |
| 97 | ''NK_LFD_LAYOUT'' | Layout-Deklaration |
| 98 | ''NK_LFD_PROPERTY'' | Widget-Property |
| 99 | ''NK_LFD_SIGNAL'' | Signal-Handler-Binding |
**Compiler-Direktiven**
^ ID ^ Name ^ Bedeutung ^
| 103 | ''NK_AT_IF'' | ''@if''-Direktive |
| 104 | ''NK_AT_DIRECTIVE'' | Allgemeine ''@''-Direktive |
| 105 | ''NK_AT_ENERGY'' | ''@energy''-Attribut |
==== Internes Node-Record-Layout ====
Jeder Parser-Knoten belegt 88 Bytes im internen nodes-Buffer:
^ Offset ^ Bytes ^ Feld ^ Bedeutung ^
| +0 | 8 | ''kind'' | Knotentyp (NK_*-ID) |
| +8 | 8 | ''c0'' | Index des ersten Knotens in Slot 0 (oder -1) |
| +16 | 8 | ''c1'' | Index des ersten Knotens in Slot 1 (oder -1) |
| +24 | 8 | ''c2'' | Index des ersten Knotens in Slot 2 (oder -1) |
| +32 | 8 | ''c3'' | Index des ersten Knotens in Slot 3 (oder -1); bei extern fn: Library-Span |
| +40 | 8 | ''next'' | Nächster Sibling in der Sibling-Kette (-1 = Ende) |
| +48 | 8 | ''tok'' | Token-Index (Verweis in Token-Buffer) |
| +56 | 8 | ''ival'' | Integer-Wert (Literal-Wert, Operator-Code o.ä.) |
| +64 | 8 | ''soff'' | Offset des String-Werts im Quelltext |
| +72 | 8 | ''slen'' | Länge des String-Werts (0 = kein String) |
| +80 | 8 | ''pub'' | ≠ 0 wenn ''pub''-deklariert |
Token-Records sind 40 Bytes; Zeile bei +32, Start-Position (für col) bei +8.
==== AstJson-Klasse (API) ====
Die Klasse ist in ''src/backend/ast_json.lyx'' als ''pub type AstJson'' definiert:
^ Funktion ^ Beschreibung ^
| ''AjInit(nodes, src, toks, tokCount, fd)'' | Initialisiert mit Parser-Daten; ''fd'': Ausgabe-FD (1 = stdout) |
| ''AjDeinit()'' | Flusht Puffer und gibt Scratch-Speicher frei |
| ''AjDump(root: int64)'' | Gibt den gesamten AST ab Knoten ''root'' als JSON-Array aus |
| ''pretty: bool'' | Auf ''true'' setzen für 2-Leerzeichen-Einrückung (''%%--%%ast-json-pretty'') |
Intern: Write-Batch-Buffer (4096 Bytes) reduziert sys_write-Aufrufe. Dezimal-Konvertierung über 24-Byte Scratch-Buffer.
----
===== --error-json =====
Implementiert in ''src/error_collector.lyx'' (Klasse ''ErrorCollector'') und in ''src/sema.lyx'' (''errColl''-Integration).
==== Schnellstart ====
# Fehler als JSON ausgeben statt Freitext
lyxc main.lyx --error-json
# In CI: JSON in Datei speichern, Exit-Code auswerten
lyxc main.lyx --error-json > errors.json
echo "Exit: $?" # 0 = ok, 1 = Sema-Fehler
# Mit jq: nur Fehler-Messages extrahieren
lyxc main.lyx --error-json | jq '.errors[].message'
==== JSON-Struktur ====
{
"errors": [
{
"errorId": "LYX-S0001",
"category": "sema",
"severity": "error",
"message": "Unbekannter Bezeichner 'foo'",
"locations": [{"file": "main.lyx", "line": 42, "col": 0}],
"expected": null,
"actual": null,
"suggestion": null,
"astPath": null
},
{
"errorId": "LYX-W0001",
"category": "sema",
"severity": "warning",
"message": "Variable 'x' wird nie gelesen",
"locations": [{"file": "main.lyx", "line": 7, "col": 0}],
"expected": null,
"actual": null,
"suggestion": null,
"astPath": null
}
],
"errorCount": 1,
"warningCount": 1
}
**Toplevel-Felder:**
^ Feld ^ Typ ^ Bedeutung ^
| ''errors'' | array | Alle Einträge (Fehler + Warnungen + Notes), chronologisch |
| ''errorCount'' | integer | Anzahl Einträge mit ''severity: "error"'' |
| ''warningCount'' | integer | Anzahl Einträge mit ''severity: "warning"'' |
**Pro Eintrag:**
^ Feld ^ Typ ^ Bedeutung ^
| ''errorId'' | string | Eindeutige ID: ''LYX-S####'' / ''LYX-P####'' / ''LYX-C####'' / ''LYX-W####'' |
| ''category'' | ''sema'' / ''parser'' / ''codegen'' | Compiler-Phase, in der der Fehler entstand |
| ''severity'' | ''error'' / ''warning'' / ''note'' | Schweregrad |
| ''message'' | string | Fehlertext (JSON-escaped) |
| ''locations'' | array | Immer genau 1 Eintrag mit ''file'', ''line'', ''col'' |
| ''expected'' | null | Reserviert (noch nicht befüllt) |
| ''actual'' | null | Reserviert (noch nicht befüllt) |
| ''suggestion'' | null | Reserviert (noch nicht befüllt) |
| ''astPath'' | null | Reserviert (noch nicht befüllt) |
==== Error-ID-Schema ====
^ Präfix ^ Kategorie/Schwere ^ Beispiel ^
| ''LYX-S####'' | Sema-Fehler (4-stellige Sequenznummer) | ''LYX-S0001'', ''LYX-S0042'' |
| ''LYX-P####'' | Parser-Fehler | ''LYX-P0001'' |
| ''LYX-C####'' | Codegen-Fehler | ''LYX-C0001'' |
| ''LYX-W####'' | Warnung (unabhängig von Kategorie) | ''LYX-W0001'' |
Sequenznummern sind getrennt für Fehler (''errSeq'') und Warnungen (''warnSeq'') und beginnen bei 1. Fehler-IDs sind daher innerhalb einer Compiler-Sitzung eindeutig.
==== Verhalten ====
* ''--error-json'' aktiviert den ''ErrorCollector'' nur für die **Semantic Analysis**. Parse-Fehler werden weiterhin als Text ausgegeben (der Collector wird erst nach dem Parsen initialisiert).
* Bei Sema-Fehlern: JSON auf stdout, Exit-Code 1.
* Bei Erfolg (kein Fehler): kein JSON-Output, Compilation läuft normal weiter.
* ''col'' ist derzeit immer 0 (Spalten-Information aus sema nicht weitergeleitet).
* ''file'' ist ''null'' für das Root-Modul; Dateiname bei via ''import'' eingebundenen Modulen.
==== ErrorCollector-Klasse (API) ====
Die Klasse ist in ''src/error_collector.lyx'' als ''pub type ErrorCollector'' definiert:
^ Funktion ^ Beschreibung ^
| ''EcInit()'' | Initialisiert: 64 Records initial (wächst automatisch); 8 192 Byte String-Pool |
| ''EcAdd(cat, sev, file, line, msg)'' | Fügt Eintrag hinzu; ''file'' darf 0 (Null-Ptr = Root-Modul) sein; ''msg'' muss NUL-terminiert sein |
| ''EcCount(): int64'' | Anzahl der bisher gesammelten Einträge |
| ''EcEmitJson(fd: int64)'' | Gibt JSON auf ''fd'' aus (''fd = 1'' für stdout) |
Konstanten (''pub con''):
^ Konstante ^ Wert ^ Bedeutung ^
| ''EC_CAT_PARSER'' | 0 | Kategorie: Parser |
| ''EC_CAT_SEMA'' | 1 | Kategorie: Semantic Analysis |
| ''EC_CAT_CODEGEN'' | 2 | Kategorie: Code-Generierung |
| ''EC_SEV_ERROR'' | 0 | Schwere: Fehler |
| ''EC_SEV_WARNING'' | 1 | Schwere: Warnung |
| ''EC_SEV_NOTE'' | 2 | Schwere: Hinweis |
| ''EC_REC_SIZE'' | 80 | Bytes pro Error-Record |
Error-Record-Layout (80 Bytes, flat buffer):
^ Offset ^ Feld ^ Bedeutung ^
| +0 | ''category'' | EC_CAT_* |
| +8 | ''severity'' | EC_SEV_* |
| +16 | ''line'' | Quellzeile |
| +24 | ''col'' | Spalte (derzeit immer 0) |
| +32 | ''filePtr'' | Zeiger auf externen Dateinamen-String (nicht owned) |
| +40 | ''msgOff'' | Offset der Meldung im String-Pool |
| +48 | ''msgLen'' | Länge der Meldung |
| +56 | ''seqNum'' | 1-basierte Sequenznummer (getrennt für Fehler/Warnungen) |
----
===== Kombination beider Flags =====
''%%--%%ast-json'' und ''%%--%%error-json'' schließen sich aus: ''%%--%%ast-json'' beendet vor der Sema, daher gibt es keine Sema-Fehler zu sammeln. Für Linting mit strukturierter Ausgabe:
# Nur Sema-Prüfung mit JSON-Fehlerausgabe (kein Binary)
lyxc main.lyx --error-json --lint-only
# AST ausgeben und anschließend separat auf Fehler prüfen
lyxc main.lyx --ast-json > ast.json
lyxc main.lyx --error-json > errors.json || true
----
===== Einsatzbeispiele =====
==== KI-Agent: alle Funktionsnamen extrahieren ====
lyxc server.lyx --ast-json | jq -r '
.[] | select(.kind == "NK_FUNC_DECL") | .sval
'
==== KI-Agent: public API als Liste ====
lyxc server.lyx --ast-json | jq -r '
[.[] | select(.kind == "NK_FUNC_DECL" and .pub == true) | .sval] | join("\n")
'
==== IDE-Plugin: strukturierte Fehler in Diagnostics umwandeln ====
lyxc main.lyx --error-json 2>/dev/null | jq '
.errors[] | {
file: .locations[0].file,
line: .locations[0].line,
severity: .severity,
message: .message
}
'
==== Build-System: Fehler zählen ====
result=$(lyxc main.lyx --error-json 2>/dev/null)
echo $result | jq '"Fehler: \(.errorCount), Warnungen: \(.warningCount)"'
----
===== Einschränkungen =====
^ Thema ^ Details ^
| Parse-Fehler / ''%%--%%error-json'' | Parse-Fehler landen **nicht** im JSON; der ErrorCollector startet erst nach dem Parser |
| ''col'' immer 0 | Spaltennummer wird von sema derzeit nicht an EcAdd weitergegeben |
| ''expected'' / ''actual'' / ''suggestion'' / ''astPath'' | Felder sind reserviert und immer ''null'' (für spätere Erweiterungen) |
| Keine Sema bei ''%%--%%ast-json'' | Der AST zeigt den **Parser-Zustand** — semantische Typen und aufgelöste Symbole fehlen |
| Kein ''%%--%%ast-json'' + ''%%--%%error-json'' | Sinnlos kombinierbar, aber ''%%--%%ast-json'' endet vor der Sema (kein Fehler-JSON wird je ausgegeben) |
| stderr unverändert | Beide Flags beeinflussen nur stdout; Debug-Ausgaben gehen weiterhin auf stderr |
Letzte Aktualisierung: 2026-06-13