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 |
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<T, E>. 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: int64[5] := [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: uint8[4096];
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 (scaled, err): (int64, int64) := SafeDiv(raw * 1013, 900) |> unwrap_tuple;
if (err != 0) {
return ErrInt64(ERR_OVERFLOW);
}
return OkInt64(scaled);
}
@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.
