Lyx – Schleifen (Loops)

Lyx bietet drei Schleifenformen für unterschiedliche Anwendungsfälle: while für bedingungsgesteuerte Schleifen, for für Zählschleifen mit bekanntem Bereich und repeat-until für fußgesteuerte Schleifen. Für sicherheitskritischen Code ergänzt das limit-Keyword jede Schleife um eine garantierte Abbruchbedingung – eine Voraussetzung für WCET-Analyse und DO-178C-Zertifizierung.

1. while – Bedingungsgesteuerte Schleife

while prüft die Bedingung vor jedem Durchlauf. Der Block wird ausgeführt, solange die Bedingung true ergibt. Ergibt sie beim ersten Test false, wird der Block kein einziges Mal ausgeführt.

unit schleifen;
import std.io;

fn main(): int64 {
    var x: int64 := 0;
    while (x < 5) {
        PrintInt(x);   // 0 1 2 3 4
        x++;
    }
    return 0;
}

Typische Verwendung

// Warten auf eine externe Bedingung
while (!DeviceReady()) {
    Sleep(10);
}

// Eingabe-Validierungs-Schleife
var input: int64 := ReadInt();
while (input < 0 | input > 100) {
    PrintStr("Ungültig. Bitte 0–100 eingeben:");
    input := ReadInt();
}

2. while – Bounded Loop (limit)

In sicherheitskritischen Bereichen muss jede Schleife nachweislich terminieren. Das limit-Keyword setzt eine harte Obergrenze für die Anzahl der Iterationen. Auch wenn die Bedingung noch true wäre, bricht die Schleife nach N Durchläufen garantiert ab.

Syntax: while (Bedingung) limit(Konstante) { … }

@flight_crit
fn WaitForSensor(): bool {
    var ready: bool := false;
    while (!ready) limit(500) {
        ready := PollSensor();
    }
    return ready;
}

 
Zertifizierungs-Hinweis (DO-178C):
In Modulen mit @dal(A) oder @dal(B) prüft der Compiler mit –stack-check, ob alle Schleifen entweder durch ein limit oder durch einen statisch beweisbaren Abbruch terminieren. Eine while (true)- Schleife ohne limit erzeugt in diesen Modulen einen Compiler-Fehler.

limit in Kombination mit break

limit wirkt zusätzlich zum normalen Abbruch durch break oder das Ende der Bedingung. Die Schleife endet beim ersten dieser Ereignisse.

var found: int64 := -1;
var i: int64 := 0;
while (i < len(buffer)) limit(1024) {
    if (buffer[i] = 0xFF) {
        found := i;
        break;
    }
    i++;
}

3. for – Zählschleife

for ist für Schleifen mit bekanntem Zählbereich optimiert. Die Laufvariable ist innerhalb des Blocks sichtbar und wird automatisch inkrementiert bzw. dekrementiert.

Aufwärts zählen (to)

for i := 0 to 9 do {
    PrintInt(i);   // 0 1 2 3 4 5 6 7 8 9
}

Die Obergrenze ist inklusive: for i := 0 to 9 erzeugt genau 10 Durchläufe (i = 0, 1, …, 9).

Abwärts zählen (downto)

for i := 10 downto 1 do {
    PrintInt(i);   // 10 9 8 7 6 5 4 3 2 1
}

for ist immer bounded

Da Ober- und Untergrenze beim Eintritt in die Schleife feststehen, ist eine for-Schleife immer terminierend – kein limit erforderlich. Der Compiler erkennt dies automatisch und gibt keine WCET-Warnung aus.

Array-Iteration (index-basiert)

var temps: [7]f64;
temps[0] := 20.1;  temps[1] := 21.3;  temps[2] := 19.8;
temps[3] := 22.0;  temps[4] := 23.5;  temps[5] := 20.9;  temps[6] := 21.7;

var sum: f64 := 0.0;
for i := 0 to 6 do {
    sum := sum + temps[i];
}
PrintFloat(sum / 7.0);   // Durchschnittstemperatur

Verschachtelte for-Schleifen

// Matrix-Multiplikation (vereinfacht, 3×3)
var a: [3][3]f64;
var b: [3][3]f64;
var c: [3][3]f64;

for i := 0 to 2 do {
    for j := 0 to 2 do {
        var acc: f64 := 0.0;
        for k := 0 to 2 do {
            acc := acc + a[i][k] * b[k][j];
        }
        c[i][j] := acc;
    }
}

4. repeat-until – Fußgesteuerte Schleife

repeat-until prüft die Abbruchbedingung nach jedem Durchlauf. Der Block wird mindestens einmal ausgeführt, auch wenn die Bedingung beim ersten Test true wäre.

var choice: int64;
repeat {
    PrintStr("Menü: 1=Start, 2=Stop, 3=Exit");
    choice := ReadInt();
} until (choice >= 1 & choice <= 3);

Vergleich: while vs. repeat-until

Kriterium while repeat-until
Prüfzeitpunkt Vor dem Körper (kopfgesteuert) Nach dem Körper (fußgesteuert)
Min. Durchläufe 0 1
Typischer Einsatz Warte-Schleifen, Stream-Verarbeitung Menüs, Eingabe-Validierung, Protokoll-Handshake
Bounded (limit) ✅ möglich ✅ möglich

repeat-until mit limit

@flight_crit
fn Handshake(): bool {
    var ack: bool := false;
    repeat {
        SendPing();
        ack := WaitAck();
    } until (ack) limit(10);
    return ack;
}

5. break & continue – Schleifensteuerung

break – Schleife sofort beenden

break verlässt die innerste laufende Schleife sofort und setzt die Ausführung hinter der schließenden }, fort.

var result: int64 := -1;
var i: int64 := 0;
while (i < 100) {
    if (IsPrime(i)) {
        result := i;
        break;           // Erstes Ergebnis gefunden – fertig
    }
    i++;
}
PrintInt(result);

continue – Iteration überspringen

continue springt direkt zum nächsten Schleifendurchlauf und überspringt den restlichen Körper der aktuellen Iteration.

for i := 0 to 99 do {
    if (i mod 2 = 0) { continue; }   // Gerade Zahlen überspringen
    PrintInt(i);                       // Gibt nur ungerade Zahlen aus: 1 3 5 … 99
}

Verschachtelte Schleifen: break bricht nur die innerste Schleife

var found: bool := false;
for row := 0 to 9 do {
    for col := 0 to 9 do {
        if (matrix[row][col] = 0) {
            found := true;
            break;       // Verlässt nur die innere (col-)Schleife
        }
    }
    if (found) { break; }   // Verlässt die äußere (row-)Schleife
}

6. Endlosschleifen – Hauptschleife (Event Loop)

Eine while (true)-Schleife ohne limit repräsentiert eine Endlosschleife, die typischerweise als Hauptschleife (Event Loop, Control Loop) in Embedded-Systemen und Servern eingesetzt wird.

fn main(): int64 {
    SystemInit();
    while (true) {         // Hauptschleife – läuft bis Power-Off
        var event := PollEvent();
        HandleEvent(event);
        UpdateOutputs();
    }
    return 0;
}

 
In @flight_crit- und @dal(A/B)-Modulen ist while (true) ohne limit verboten.
Die Hauptschleife eines Steuergeräts lebt typischerweise im @dal(C)-Hauptmodul; die aufgerufenen Handler-Funktionen tragen die höheren DAL-Attribute.

7. @parallel – SIMD-Vektorisierung von Schleifen

Das @parallel-Pragma weist den Compiler an, eine Schleife zu vektorisieren (AVX2 auf x86_64, NEON auf ARM64). Voraussetzung: keine Datenabhängigkeiten zwischen den Iterationen.

unit dsp;

fn ScaleSignal(samples: [1024]f32, gain: f32) {
    @parallel
    for i := 0 to 1023 do {
        samples[i] := samples[i] * gain;   // 8 Iterationen pro AVX2-Instruktion
    }
}

fn Threshold(data: [512]f64, limit_val: f64): [512]bool {
    var result: [512]bool;
    @parallel
    for i := 0 to 511 do {
        result[i] := data[i] > limit_val;
    }
    return result;
}

@parallel ist nicht zulässig, wenn Iterationen voneinander abhängen (z. B. Akkumulation in sum := sum + x[i]) – der Compiler warnt in diesem Fall.

8. Energy-Aware Loop Unrolling

Der Compiler passt das Loop Unrolling automatisch an das Energy-Level der umgebenden Funktion an. Unrolling reduziert den Branch-Overhead, vergrößert aber den Code.

Energy-Level Unrolling-Faktor Beschreibung
1 (Minimal) Minimaler Code-Footprint, schont I-Cache
2 Leichtes Unrolling, geringer Overhead
3 (Standard) Ausgewogen – Standard ohne @energy-Pragma
4 Aggressives Unrolling, nutzt mehrere Ausführungseinheiten
5 (Extreme) 8× + SIMD Maximaler Durchsatz; auch ohne @parallel vektorisiert

@energy(5)
fn ProcessHighFreq(samples: [256]f32) {
    for i := 0 to 255 do {
        samples[i] := samples[i] * 2.0f32;
    }
    // Compiler unrollt 8× und kombiniert mit SIMD: ~32 Iterationen à 8 Elemente
}

@energy(1)
fn LowPowerScan(data: [64]uint8): int64 {
    var count: int64 := 0;
    for i := 0 to 63 do {
        if (data[i] != 0u8) { count++; }
    }
    return count;
    // Compiler unrollt 2× – minimal; spart Code-Größe und I-Cache
}

9. Schleifen in Safety-Code

Schleifentyp @flight_crit @dal(A) @dal(B) @dal(C/D)
for (bekannte Grenzen)
while mit limit
repeat-until mit limit
while ohne limit ❌ Fehler ❌ Fehler ❌ Fehler ⚠️ Warnung
while (true) ohne limit ❌ Fehler ❌ Fehler ❌ Fehler ⚠️ Warnung
@parallel-Schleife ❌ Verboten ❌ Verboten ⚠️ Warnung

WCET-Annotation

Der –stack-check- und –wcet-Pass analysiert die Iterationszahl aller Schleifen:

$ lyxc --wcet --call-graph main.lyx

Loop-Analyse:
  WaitForSensor (while, limit=500)   →  max.  500 Iter.  ✅ beschränkt
  Handshake     (repeat, limit=10)   →  max.   10 Iter.  ✅ beschränkt
  ScaleSignal   (for, 0..1023)       →  exakt 1024 Iter. ✅ statisch bekannt
  MainLoop      (while true, kein limit) →  unbeschränkt ✗ @dal(A): Fehler

10. Typische Schleifenmuster

Lineare Suche

fn Find(arr: [256]int64, target: int64): int64 {
    for i := 0 to 255 do {
        if (arr[i] = target) { return i; }
    }
    return -1;
}

Akkumulation / Reduktion

fn Sum(arr: [64]int64): int64 {
    var total: int64 := 0;
    for i := 0 to 63 do {
        total := total + arr[i];
    }
    return total;
}

fn Max(arr: [64]int64): int64 {
    var best := arr[0];
    for i := 1 to 63 do {
        if (arr[i] > best) { best := arr[i]; }
    }
    return best;
}

Gleitender Mittelwert (Sliding Window)

fn MovingAvg(data: [128]f64, window: int64): [128]f64 {
    var result: [128]f64;
    for i := 0 to 127 do {
        var sum: f64 := 0.0;
        var count: int64 := 0;
        var j := i - window + 1;
        if (j < 0) { j := 0; }
        while (j <= i) {
            sum := sum + data[j];
            count++;
            j++;
        }
        result[i] := sum / count as f64;
    }
    return result;
}

Ringpuffer-Drain

fn DrainBuffer(buf: ^uint8, head: int64, tail: int64, size: int64): int64 {
    var processed: int64 := 0;
    while (head != tail) limit(4096) {
        ProcessByte(buf[head]);
        head := (head + 1) mod size;
        processed++;
    }
    return processed;
}

11. Vollständiges Beispiel: Sensor-Datenfilter

Ein typischer Embedded-Regelkreis kombiniert alle Schleifenformen:

unit sensor_filter;
import std.io;
import std.math;

con SAMPLE_COUNT: int64 := 64;
con THRESHOLD:    f64   := 50.0;
con MAX_RETRIES:  int64 := 10;

@flight_crit
@dal(C)
@stack_limit(2048)
fn AcquireAndFilter(): f64 {
    var samples: [64]f64;

    // 1. Daten erfassen – repeat-until: mindestens ein Versuch
    var ok: bool := false;
    repeat {
        ok := SensorReady();
    } until (ok) limit(MAX_RETRIES);

    if (!ok) { return -1.0; }   // Sensor nicht bereit

    // 2. Samples einlesen – for: exakt bekannte Anzahl
    for i := 0 to SAMPLE_COUNT - 1 do {
        samples[i] := ReadSensor();
    }

    // 3. Ausreißer entfernen – for mit continue
    var sum: f64 := 0.0;
    var count: int64 := 0;
    for i := 0 to SAMPLE_COUNT - 1 do {
        if (samples[i] < 0.0 | samples[i] > THRESHOLD) {
            continue;   // Ausreißer überspringen
        }
        sum := sum + samples[i];
        count++;
    }

    if (count = 0) { return -1.0; }
    return sum / count as f64;
}

fn main(): int64 {
    SystemInit();

    // 4. Haupt-Regelschleife – while ohne limit (im @dal(C)-Kontext erlaubt)
    while (true) {
        var value := AcquireAndFilter();
        if (value >= 0.0) {
            PrintFloat(value);
            SetActuator(value);
        } else {
            PrintStr("Sensorfehler");
        }
        Sleep(100);
    }

    return 0;
}

12. Zusammenfassung

Schleifentyp Prüfung Min. Durchläufe Bounded Typischer Einsatz
while (cond) Vor dem Körper 0 limit(N) Warten, Stream-Verarbeitung
while (true) limit(N) Event-Loop, Hauptschleife
for i := a to b Vor dem Körper 0 (wenn a > b) Immer (b-a+1) Array-Iteration, Zählschleifen
for i := b downto a Vor dem Körper 0 (wenn b < a) Immer (b-a+1) Rückwärts-Iteration
repeat-until Nach dem Körper 1 limit(N) Eingabe, Handshake, Protokoll

Weiterführende Seiten: