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.

 
Globale Arrays und Structs tragen seit lyxc 1.0.17C (#1256, #1299). Auf Modulebene deklarierte Aggregate verhalten sich jetzt wie lokale: var q: [8]int64; nimmt einen schreibenden Indexzugriff an, ein Initialisierer (:= [1,2,3,4,5,6,7,8]) bleibt erhalten, und ein Feldzugriff auf einen globalen Struct (s.x := 3) wirkt.

Bis 1.0.16x stürzte der schreibende Zugriff ab, der Initialisierer wurde verworfen, und Struct-Felder blieben still auf 0.

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) {
        PrintLn(IntToStr(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) {
    Print("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):
(Alle DAL-Stufen und Attribute: Attributes & Pragmas.) In Modulen mit @dal(A) oder @dal(B) prüft der Compiler, 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 {
    PrintLn(IntToStr(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 {
    PrintLn(IntToStr(i));  // 10 9 8 7 6 5 4 3 2 1
}

Bereichsform (in a..b)

Seit lyxc 1.0.14 gibt es zusätzlich die Bereichsform. Beide Grenzen zählen mit:

for i in 0..3 {
    PrintLn(IntToStr(i));   // 0 1 2 3
}

Derselbe Bereich lässt sich auch als Prüfung verwenden — x in a..b ergibt einen Wahrheitswert:

if (x in 1..10) { PrintLn("im Bereich"); }

Die Bereichsform erwartet einen Bereich, keine Sammlung: for v in a über ein Array meldet for … in erwartet einen Bereich. Über Arrays läuft man mit Index (siehe unten).

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 {
    Print("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;
    var versuche: int64 := 0;
    repeat {
        SendPing();
        ack := WaitAck();
        versuche := versuche + 1;
    } until (ack || versuche >= 10);
    return ack;
}

 
limit(N) gibt es nur an while. An repeat/until meldet der Compiler limit(N) gibt es nur an while, nicht an repeat/until. Eine Obergrenze wird dort über einen eigenen Zähler in der Abbruchbedingung ausgedrückt — wie oben. Frühere Fassungen dieser Seite zeigten until (ack) limit(10);.

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++;
}
Print(IntToStr(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 % 2 == 0) { continue; }   // Gerade Zahlen überspringen
    PrintLn(IntToStr(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.0;
    }
    // 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] != 0) { 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 --call-graph-Pass analysiert Aufrufe und Iterationszahlen:

$ lyxc --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: int64, head: int64, tail: int64, size: int64): int64 {
    var processed: int64 := 0;
    while (head != tail) limit(4096) {
        ProcessByte(buf[head]);
        head := (head + 1) % size;
        processed++;
    }
    return processed;
}

11. Vollständiges Beispiel: Sensor-Datenfilter

unit sensor_filter;
import std.io;
import std.math;
import std.time;   // Sleep

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

// Unter @flight_crit sind LOKALE Felder nicht erlaubt: sie entstuenden per
// mmap, nicht auf dem Stapel. Der Puffer wird deshalb auf Modulebene
// vorgehalten — dasselbe Muster, das sicherheitskritischer Code ohnehin
// verlangt (keine Belegung zur Laufzeit).
var samples: [64]f64;

@flight_crit
@dal(C)
@stack_limit(2048)
fn AcquireAndFilter(): f64 {

    // 1. Daten erfassen – repeat-until: mindestens ein Versuch
    var ok: bool := false;
    var versuche: int64 := 0;
    repeat {
        ok := SensorReady();
        versuche := versuche + 1;
    } until (ok || versuche >= 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 {
            Print("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 eigener Zähler Eingabe, Handshake, Protokoll

Weiterführende Seiten:

Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).