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.


Referenz