====== 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