====== Lyx – Fehlerbehandlung ====== Lyx kennt kein ''try''/''catch''/''throw''. Das ist eine bewusste Designentscheidung: Exceptions erzeugen nicht-linearen Kontrollfluss, der zur Compile-Zeit schwer nachzuverfolgen ist, WCET-Analysen erschwert und in sicherheitskritischen Systemen verboten ist. Stattdessen setzt Lyx auf **explizite Fehlerbehandlung**: Fehler sind normale Rückgabewerte. Sie werden dort behandelt, wo sie auftreten — nicht irgendwo im Call-Stack. Das macht Fehlerbehandlung sichtbar, testbar und zertifizierbar. Lyx bietet vier Mechanismen, je nach Kontext: ^ Mechanismus ^ Einsatz ^ Safety ^ | **Tuple-Return** (''value, bool'') | Einfache Fehler, schneller Code | DAL-A bis DAL-E | | **Result-Typ** (''std.result'') | Strukturierte Fehler mit Fehlercode | DAL-A bis DAL-E | | **POSIX-errno** (''std.error'') | Syscalls, Betriebssystem-Interaktion | DAL-C bis DAL-E | | **panic()** | Nicht behebbare Fehler, Invariantverletzungen | Kontrollierter Abbruch | → [[lyx_-_programmiersprache:pattern-matching|Pattern Matching]] · [[lyx_-_programmiersprache:do-178c|DO-178C]] · [[lyx_-_programmiersprache:datentypen|Datentypen]] · [[lyx_-_programmiersprache:funktionen|Funktionen]] ---- ===== 1. Tuple-Return — Einfachster Weg ===== Die leichtgewichtigste Form der Fehlerbehandlung: Eine Funktion gibt ihr Ergebnis zusammen mit einem ''bool'' zurück, der Erfolg oder Misserfolg signalisiert. Kein Import, kein Overhead, direkt lesbar. fn Divide(a: int64, b: int64): (int64, bool) { if (b = 0) { return (0, false); // Fehlerfall: Division durch null } return (a / b, true); // Erfolg } fn ParsePort(s: int64): (int64, bool) { var port: int64 := StrToInt(s); if (port < 1 | port > 65535) { return (0, false); } return (port, true); } fn main(): int64 { var (result, ok): (int64, bool) := Divide(10, 3); if (!ok) { PrintStr("Fehler: Division durch null\n"); return 1; } PrintStr("Ergebnis: "); PrintInt(result); PrintStr("\n"); var (port, valid): (int64, bool) := ParsePort("8080"); if (valid) { PrintStr("Port: "); PrintInt(port); PrintStr("\n"); } return 0; } Das Tuple-Pattern eignet sich für einfache, lokale Fehler. Wenn mehr Kontext über die Art des Fehlers gebraucht wird — z.B. warum etwas fehlschlug — ist der Result-Typ die bessere Wahl. ---- ===== 2. Result-Typen (std.result) ===== ''std.result'' stellt konkrete Result-Strukturen bereit — nach dem Vorbild von Rusts ''Result''. Jeder Result-Typ enthält ein ''success''-Flag, einen Wert und einen Fehlercode. ==== Verfügbare Typen ==== ^ Typ ^ Erfolgswert ^ Konstruktoren ^ | ''ResultInt64'' | ''int64'' | ''OkInt64(value)'', ''ErrInt64(code)'' | | ''ResultBool'' | ''bool'' | ''OkBool(value)'', ''ErrBool(code)'' | | ''ResultVec2'' | ''Vec2'' | ''OkVec2(value)'', ''ErrVec2(code)'' | | ''OptionInt64'' | ''int64'' | ''SomeInt64(value)'', ''NoneInt64()'' | | ''OptionVec2'' | ''Vec2'' | ''SomeVec2(value)'', ''NoneVec2()'' | ==== Fehlercodes ==== ''std.result'' definiert eine Menge standardisierter Fehlercodes: ^ Konstante ^ Code ^ Bedeutung ^ | ''ERR_NONE'' | ''0'' | Kein Fehler | | ''ERR_INVALID_INPUT'' | ''2'' | Ungültige Eingabe | | ''ERR_OUT_OF_BOUNDS'' | ''3'' | Index außerhalb des gültigen Bereichs | | ''ERR_DIVISION_BY_ZERO'' | ''4'' | Division durch null | | ''ERR_OVERFLOW'' | ''5'' | Überlauf | | ''ERR_UNDERFLOW'' | ''6'' | Unterlauf | | ''ERR_PARSE_ERROR'' | ''7'' | Parsing fehlgeschlagen | | ''ERR_NOT_FOUND'' | ''8'' | Nicht gefunden | | ''ERR_PERMISSION_DENIED'' | ''10'' | Zugriff verweigert | | ''ERR_IO'' | ''11'' | I/O-Fehler | | ''ERR_OUT_OF_MEMORY'' | ''12'' | Kein Speicher mehr | ==== Grundlegende Verwendung ==== import std.result; import std.io; fn SafeSqrt(x: f64): (f64, int64) { if (x < 0.0) { return (0.0, ERR_INVALID_INPUT); } return (math.Sqrt(x), ERR_NONE); } fn ReadSensorValue(port: int64): ResultInt64 { if (port < 0 | port > 255) { return ErrInt64(ERR_INVALID_INPUT); } var raw: int64 := MemRead32(port); if (raw = 0xFFFFFFFF) { return ErrInt64(ERR_IO); // Sensor antwortet nicht } return OkInt64(raw); } fn main(): int64 { var r: ResultInt64 := ReadSensorValue(0x40); if (ResultInt64IsOk(r)) { PrintStr("Wert: "); PrintInt(ResultInt64Unwrap(r)); PrintStr("\n"); } else { PrintStr("Fehler: "); PrintStr(ErrorCodeToString(ResultInt64Error(r))); PrintStr("\n"); } return 0; } ==== Unwrap-Varianten ==== Statt immer ein ''if (IsOk)'' zu schreiben gibt es mehrere Auspack-Varianten: import std.result; fn main(): int64 { var r: ResultInt64 := ReadSensorValue(0x40); // Unwrap: gibt Wert zurück oder bricht mit panic() ab var val1: int64 := ResultInt64Unwrap(r); // UnwrapOr: gibt Wert zurück oder Fallback bei Fehler var val2: int64 := ResultInt64UnwrapOr(r, 0); // Expect: wie Unwrap, aber mit eigener Fehlermeldung var val3: int64 := ResultInt64Expect(r, "Sensor konnte nicht gelesen werden"); return 0; } ^ Methode ^ Verhalten bei Fehler ^ | ''Unwrap'' | panic() mit generischer Meldung | | ''UnwrapOr(fallback)'' | Gibt Fallback-Wert zurück — kein Abbruch | | ''Expect(msg)'' | panic() mit eigener Fehlermeldung | ==== Verkettung mit AndThen ==== ''ResultInt64AndThen'' ermöglicht das Verketten von Operationen: Wenn der vorherige Schritt erfolgreich war, wird die nächste Operation ausgeführt. Bei Fehler wird der Fehler durchgereicht. import std.result; fn ValidateRange(v: int64): ResultInt64 { if (v < 0 | v > 1000) { return ErrInt64(ERR_OUT_OF_BOUNDS); } return OkInt64(v); } fn ScaleValue(v: int64): ResultInt64 { return OkInt64(v * 10); } fn main(): int64 { // Kurzform: Schritt für Schritt, Fehler wird automatisch weitergereicht var r: ResultInt64 := ReadSensorValue(0x40); r := ResultInt64AndThen(r, ValidateRange as int64); r := ResultInt64AndThen(r, ScaleValue as int64); if (ResultInt64IsOk(r)) { PrintStr("Skalierter Wert: "); PrintInt(ResultInt64Unwrap(r)); PrintStr("\n"); } else { PrintStr("Fehlgeschlagen: "); PrintStr(ErrorCodeToString(ResultInt64Error(r))); PrintStr("\n"); } return 0; } ---- ===== 3. Option-Typen — "Wert oder nichts" ===== Während Result einen Fehlergrund trägt, kodiert ''OptionInt64'' nur das Vorhandensein oder Fehlen eines Wertes — ohne Fehlercode. Einsatz: Suchen, optionale Konfigurationswerte, nullable Rückgaben. import std.result; import std.io; fn FindFirst(arr: int64, n: int64, target: int64): OptionInt64 { var i: int64 := 0; while (i < n) limit(65536) { var elem: int64 := (arr + i * 8) as int64; if (elem = target) { return SomeInt64(i); // Gefunden: Index zurückgeben } i := i + 1; } return NoneInt64(); // Nicht gefunden } fn main(): int64 { var data: [6]int64 := [10, 20, 30, 40, 50, 60]; var result: OptionInt64 := FindFirst(data as int64, 6, 30); if (OptionInt64IsSome(result)) { PrintStr("Gefunden an Index: "); PrintInt(OptionInt64Unwrap(result)); PrintStr("\n"); } else { PrintStr("Nicht gefunden\n"); } // UnwrapOr: Fallback wenn nicht gefunden var idx: int64 := OptionInt64UnwrapOr(result, -1); return 0; } ---- ===== 4. Sichere Arithmetik ===== ''std.result'' enthält Varianten der Grundrechenarten, die Über- und Unterläufe sowie Division durch null als ''ResultInt64'' zurückgeben — statt undefiniertes Verhalten zu produzieren. import std.result; import std.io; fn main(): int64 { // Division mit Null-Prüfung var r_div: ResultInt64 := SafeDiv(100, 0); if (ResultInt64IsErr(r_div)) { PrintStr("Fehler: "); PrintStr(ErrorCodeToString(ResultInt64Error(r_div))); PrintStr("\n"); // Ausgabe: "Division durch null" } // Multiplikation mit Überlaufprüfung var r_mul: ResultInt64 := SafeMul(9223372036854775807, 2); if (ResultInt64IsErr(r_mul)) { PrintStr("Überlauf erkannt\n"); } // Sicherer Array-Zugriff mit Grenzprüfung var arr: [5]int64 := [1, 2, 3, 4, 5]; var r_get: ResultInt64 := SafeArrayGet(arr as int64, 5, 10); // Index 10 → Out of Bounds if (ResultInt64IsErr(r_get)) { PrintStr("Zugriff außerhalb der Grenzen\n"); } // SafeArraySet var r_set: ResultBool := SafeArraySet(arr as int64, 5, 2, 99); if (ResultBoolIsOk(r_set)) { PrintStr("arr[2] gesetzt\n"); } return 0; } ^ Funktion ^ Geprüftes Szenario ^ | ''SafeAdd(a, b)'' | int64-Überlauf | | ''SafeSub(a, b)'' | int64-Unterlauf | | ''SafeMul(a, b)'' | int64-Überlauf | | ''SafeDiv(a, b)'' | Division durch null | | ''SafeMod(a, b)'' | Modulo durch null | | ''SafeArrayGet(arr, len, idx)'' | Index außerhalb [0, len) | | ''SafeArraySet(arr, len, idx, val)'' | Index außerhalb [0, len) | ---- ===== 5. POSIX-Fehler (std.error) ===== Für systemnahen Code — Dateisystem, Netzwerk, Prozesse — liefert ''std.error'' die vollständigen POSIX-errno-Codes und Hilfsfunktionen zur Fehlerauswertung. Syscalls in Lyx geben bei Fehler negative Werte zurück (Konvention: ''-(errno)''). ''CheckSyscallError'' prüft, ob ein Rückgabewert einen Fehler signalisiert, ''GetSyscallErrorMessage'' liefert den lesbaren Text. import std.error; import std.io; import std.fs; fn ReadFile(path: int64): (int64, int64) { var fd: int64 := SysOpen(path, 0, 0); if (CheckSyscallError(fd)) { return (0, GetSyscallErrorCode(fd)); } var buf: [4096]uint8; var n: int64 := SysRead(fd, buf as int64, 4096); SysClose(fd); if (CheckSyscallError(n)) { return (0, GetSyscallErrorCode(n)); } return (n, 0); } fn main(): int64 { var (bytes_read, err): (int64, int64) := ReadFile("/etc/hostname"); if (err != 0) { PrintStr("Fehler beim Lesen: "); PrintStr(GetErrorMessage(err)); PrintStr("\n"); return 1; } PrintStr("Gelesen: "); PrintInt(bytes_read); PrintStr(" Bytes\n"); return 0; } Häufige POSIX-Fehlercodes: ^ Konstante ^ Code ^ Bedeutung ^ | ''ENOENT'' | ''2'' | Datei oder Verzeichnis nicht gefunden | | ''EACCES'' | ''13'' | Zugriff verweigert | | ''EEXIST'' | ''17'' | Datei existiert bereits | | ''ENOTDIR'' | ''20'' | Kein Verzeichnis | | ''EINVAL'' | ''22'' | Ungültiges Argument | | ''EMFILE'' | ''24'' | Zu viele offene Dateien | | ''ENOSPC'' | ''28'' | Kein Speicherplatz auf Gerät | | ''ETIMEDOUT'' | ''110'' | Verbindungs-Timeout | | ''ECONNREFUSED'' | ''111'' | Verbindung abgelehnt | ---- ===== 6. panic() — Nicht behebbare Fehler ===== ''panic()'' ist kein Exception-Mechanismus — es gibt keinen ''catch''. Ein ''panic()'' beendet das Programm sofort mit einer Fehlermeldung und einem Stack-Trace. Es ist für Zustände, die **nicht auftreten dürfen** und auf einen Programmierfehler oder Hardwaredefekt hinweisen. fn GetElement(arr: int64, len: int64, index: int64): int64 { if (index < 0 | index >= len) { panic("GetElement: Index außerhalb der Grenzen"); // Kein return nötig — panic() kehrt nie zurück } return (arr + index * 8) as int64; } fn ConnectToDatabase(): int64 { var fd: int64 := OpenTCPSocket("localhost", 5432); if (fd < 0) { // Datenbank ist Pflichtinfrastruktur — ohne sie kann das Programm nicht laufen panic("Datenbankverbindung fehlgeschlagen — Programm kann nicht fortfahren"); } return fd; } ''panic()'' ist sinnvoll für: * Verletzung von Invarianten, die nie auftreten dürfen (Buffer Overflow, falscher Typ) * Programmierfehler, die im Test hätten erkannt werden sollen * Initialisierungsfehler, nach denen kein sicherer Betrieb möglich ist ''panic()'' ist **nicht** sinnvoll für: * Erwartbare Fehler wie "Datei nicht gefunden" oder "Netzwerk nicht erreichbar" * Benutzereingabe-Fehler * Jeden Fehler in sicherheitskritischem Code — dort Result-Typen verwenden Im Safety-Umfeld (DO-178C DAL-A/B) muss dokumentiert sein, unter welchen Bedingungen ''panic()'' erreichbar ist. Der Compiler-Flag ''--static-analysis'' prüft, ob ''panic()''-Pfade existieren und markiert sie im Report. ---- ===== 7. Fehlerbehandlung im Safety-Umfeld ===== Für DO-178C-zertifizierten Code gelten klare Regeln: ^ Situation ^ Empfehlung ^ Begründung ^ | Arithmetik | ''SafeAdd'', ''SafeDiv'' etc. | Überlauf und Division durch null explizit geprüft | | Array-Zugriff | ''SafeArrayGet'', ''SafeArraySet'' | Grenzprüfung ohne undefiniertes Verhalten | | Fehler in Funktionen | ''ResultInt64'' / Tuple-Return | Linearer Kontrollfluss, WCET-berechenbar | | Optionale Werte | ''OptionInt64'' | Kein Null-Pointer, kein undefinierter Zustand | | Syscalls | ''CheckSyscallError'' + errno | POSIX-konform, keine Exceptions | | Nicht behebbare Fehler | ''panic()'' — nur in Initialisierung | Dokumentiert, nicht im Regelzyklus | | Exceptions (try/catch) | Nicht verwenden | Nicht-linearer Kontrollfluss, WCET nicht berechenbar | ==== Vollständiges Beispiel: Sensor-Lese-Pipeline ==== import std.result; import std.error; import std.io; con SENSOR_PORT: int64 := 0x40; con MIN_VALUE: int64 := 100; con MAX_VALUE: int64 := 900; // Schritt 1: Rohwert lesen fn ReadRaw(port: int64): ResultInt64 { if (port < 0 | port > 255) { return ErrInt64(ERR_INVALID_INPUT); } var raw: int64 := MemRead32(port); if (raw < 0) { return ErrInt64(ERR_IO); } return OkInt64(raw); } // Schritt 2: Wertebereich prüfen fn ValidateRange(value: int64): ResultInt64 { if (value < MIN_VALUE | value > MAX_VALUE) { return ErrInt64(ERR_OUT_OF_BOUNDS); } return OkInt64(value); } // Schritt 3: Einheit umrechnen (Rohwert → Millibar) fn ConvertToMillibar(raw: int64): ResultInt64 { var r_mul: ResultInt64 := SafeMul(raw, 1013); if (ResultInt64IsErr(r_mul)) { return ErrInt64(ERR_OVERFLOW); } var r_div: ResultInt64 := SafeDiv(ResultInt64Unwrap(r_mul), 900); if (ResultInt64IsErr(r_div)) { return ErrInt64(ERR_OVERFLOW); } return r_div; } @flight_crit @stack_limit(512) fn ReadPressure(): ResultInt64 { var r: ResultInt64 := ReadRaw(SENSOR_PORT); r := ResultInt64AndThen(r, ValidateRange as int64); r := ResultInt64AndThen(r, ConvertToMillibar as int64); return r; } fn main(): int64 { var r: ResultInt64 := ReadPressure(); if (ResultInt64IsOk(r)) { PrintStr("Druck: "); PrintInt(ResultInt64Unwrap(r)); PrintStr(" mbar\n"); } else { PrintStr("Sensor-Fehler: "); PrintStr(ErrorCodeToString(ResultInt64Error(r))); PrintStr("\n"); return 1; } return 0; } Jeder Schritt der Pipeline gibt ''ResultInt64'' zurück. ''AndThen'' reicht einen Fehler automatisch durch — der nächste Schritt wird nur ausgeführt, wenn der vorherige erfolgreich war. Der Kontrollfluss bleibt vollständig linear und WCET-berechenbar. ---- ===== Referenz ===== → [[lyx_-_programmiersprache:units:result|std.result — Vollständige Funktionsreferenz]]\\ → [[lyx_-_programmiersprache:units:error|std.error — POSIX-Fehlercodes und Hilfsfunktionen]]\\ → [[lyx_-_programmiersprache:pattern-matching|Pattern Matching — match, case, default]]\\ → [[lyx_-_programmiersprache:do-178c|DO-178C — Sicherheitskritische Programmierung]]\\ → [[lyx_-_programmiersprache:funktionen|Funktionen — Tupel-Return, Higher-Order Functions]] Letzte Aktualisierung: 2026-05-22