Inhaltsverzeichnis

Lyx – Memory Management

Lyx hat keinen Garbage Collector. Das ist keine Einschränkung — es ist ein Designziel. GC-Pausen sind nicht-deterministisch: Sie können jederzeit einsetzen, beliebig lange dauern und Echtzeitgarantien zerstören. In eingebetteten Systemen, Flugsteuerungen und Hochlast-Servern ist vorhersagbares Speicherverhalten keine Option, sondern Pflicht.

Lyx gibt dem Entwickler vollständige Kontrolle über drei Speicherbereiche:

Bereich Allokation Freigabe Einsatz
Stack Automatisch bei Variablendeklaration Automatisch bei Scope-Ende Lokale Variablen, feste Arrays, Structs
Heap Explizit mit new / malloc Explizit mit dispose / free_mem Daten die Scope-Grenzen überschreiten
Statischer Speicher Zur Compile-Zeit Nie Konstanten, String-Literale, globale Puffer

1. Stack-Allokation

Der Stack ist der schnellste und sicherste Speicherbereich. Jede lokale Variable, jeder feste Array und jeder Struct liegt automatisch auf dem Stack — kein Allokationsaufruf, keine Freigabe.

fn ProcessData(): void {
    var counter:  int64 := 0;          // 8 Byte auf dem Stack
    var ratio:    f64   := 1.5;        // 8 Byte auf dem Stack
    var name:     pchar := "Sensor A"; // Zeiger (8 Byte) auf statischen String

    var readings: f64[64];             // 64 × 8 = 512 Byte auf dem Stack
    var header:   PacketHeader;        // Struct — Größe zur Compile-Zeit bekannt

    // counter, ratio, readings, header werden automatisch freigegeben
    // wenn ProcessData() zurückkehrt
}

Vorteile des Stacks:

Einschränkung: Die Größe muss zur Compile-Zeit bekannt sein. var buf: uint8[n] mit einem Laufzeit-Wert n ist nicht zulässig.

Stack-Tiefe und Scope

Lokale Variablen leben genau so lange wie ihr Scope. Beim Verlassen des inneren Blocks wird der Speicher sofort freigegeben:

fn Example(): int64 {
    var outer: int64 := 10;

    {
        var inner: int64 := 20;   // lebt nur in diesem Block
        outer := outer + inner;
    }
    // inner ist hier nicht mehr zugänglich — Speicher freigegeben

    return outer;   // 30
}


2. Stack-Limit und statische Stack-Analyse

Stack-Overflows sind in eingebetteten Systemen eine der häufigsten Fehlerquellen — und besonders heimtückisch, weil sie oft nur unter Last auftreten. @stack_limit(N) definiert das maximale Stack-Budget einer Funktion inklusive aller ihrer Aufrufe. Mit –stack-check analysiert der Compiler den gesamten Call-Graph statisch.

@stack_limit(512)
fn FilterSensorData(raw: int64, n: int64): f64 {
    var sum: f64 := 0.0;
    var buf: f64[16];          // 128 Byte — passt ins Budget

    var i: int64 := 0;
    while (i < 16 && i < n) limit(16) {
        buf[i] := (raw + i * 8) as f64;
        sum := sum + buf[i];
        i := i + 1;
    }
    return sum / 16.0;
}

@stack_limit(1024)
fn RunControlCycle(sensors: int64, n: int64): void {
    var filtered: f64 := FilterSensorData(sensors, n);  // nested call
    var result: ControlOutput := ComputeOutput(filtered);
    ApplyControl(result);
}

lyxc flight_ctrl.lyx --stack-check -o flight_ctrl

Beispielausgabe bei Grenzwert-Verletzung:

[stack-check] RunControlCycle
  direct locals:    320 bytes
  FilterSensorData: 192 bytes  (inkl. dessen Locals)
  ComputeOutput:    256 bytes
  ApplyControl:      88 bytes
  ─────────────────────────────
  worst-case total: 856 bytes / 1024 limit — OK

[stack-check] FilterSensorData
  direct locals:    192 bytes / 512 limit — OK

[stack-check] PASS — alle Limits eingehalten

Wenn ein Limit überschritten wird:

error: function 'RunControlCycle' exceeds @stack_limit:
  worst-case stack: 1280 bytes, limit: 1024 bytes
  call chain: RunControlCycle → ComputeOutput → FFTTransform (576 bytes)


3. Heap-Allokation mit new / dispose

Heap-Speicher überlebt den Scope seiner Allokation. Er wird mit new angefordert und muss mit dispose explizit freigegeben werden.

Einfache Allokation

fn main(): int64 {
    // Einzelnen Wert auf dem Heap
    var ptr: int64 := new int64;
    (ptr as int64) := 42;          // Wert schreiben

    PrintInt(ptr as int64);        // Wert lesen
    PrintStr("\n");

    dispose ptr;                   // Freigabe — danach ist ptr ungültig
    return 0;
}

Array auf dem Heap

fn AllocBuffer(size: int64): int64 {
    var buf: int64 := new uint8[size];   // Heap-Array — Größe zur Laufzeit bestimmt
    return buf;
}

fn main(): int64 {
    var n: int64 := 4096;
    var buffer: int64 := AllocBuffer(n);

    // Buffer verwenden...
    FillBuffer(buffer, n, 0);

    dispose buffer;   // Pflicht — kein GC räumt auf
    return 0;
}

Struct auf dem Heap

type Connection = struct {
    fd:       int64;
    buf:      int64;   // Zeiger auf Heap-Puffer
    buf_size: int64;
    connected: bool;
};

fn NewConnection(fd: int64): int64 {
    var conn: int64 := new Connection;
    var c: Connection := conn as Connection;
    c.fd       := fd;
    c.buf      := new uint8[8192];
    c.buf_size := 8192;
    c.connected := true;
    return conn;
}

fn FreeConnection(conn: int64): void {
    var c: Connection := conn as Connection;
    dispose c.buf;   // erst den inneren Puffer freigeben
    dispose conn;    // dann die Struct selbst
}

Reihenfolge bei Freigabe: Immer von innen nach außen freigeben. Erst den Inhalt, dann den Container — sonst Memory Leak.

4. std.alloc — Direkte Heap-Kontrolle

std.alloc gibt direkten Zugriff auf die Heap-Allokations-Primitive — ohne den new/dispose-Syntaxzucker. Das ist die Grundlage aller dynamischen Datenstrukturen in der Standardbibliothek.

import std.alloc;

fn main(): int64 {
    // malloc: Speicher allokieren (unkinitialisiert)
    var ptr: int64 := malloc(1024);
    if (ptr == 0) {
        PrintStr("Out of Memory\n");
        return 1;
    }

    // calloc: Speicher allokieren und auf 0 setzen
    var zeroed: int64 := calloc(64, 8);   // 64 int64 = 512 Byte, alle 0

    // realloc: Puffer vergrößern
    var bigger: int64 := realloc_mem(zeroed, 2048);
    if (bigger == 0) {
        free_mem(zeroed);   // realloc fehlgeschlagen — Original noch gültig
        return 1;
    }
    zeroed := bigger;

    // Speicher freigeben
    free_mem(ptr);
    free_mem(zeroed);

    return 0;
}

malloc_safe vs. malloc_orpanic

import std.alloc;

fn ProcessLargeDataset(n: int64): void {
    // malloc_safe: gibt 0 zurück bei OOM — Aufrufer entscheidet
    var buf: int64 := malloc_safe(n * 8);
    if (buf == 0) {
        PrintStr("Kein Speicher — Datensatz übersprungen\n");
        return;
    }
    // ... verarbeiten ...
    free_mem(buf);
}

fn InitSystemBuffers(): void {
    // malloc_orpanic: bricht das Programm ab bei OOM — für Pflicht-Ressourcen
    var ring_buf: int64 := malloc_orpanic(65536);
    // Falls OOM: Programm endet mit Fehlercode — kein weiterer Code wird ausgeführt
    // ring_buf ist hier garantiert gültig
}

Funktion Verhalten bei OOM Einsatz
malloc Gibt 0 zurück (kein Abbruch) Wenn OOM behandelt werden soll
malloc_safe Gibt 0 zurück Explizit: macht Null-Prüfung sichtbar
malloc_orpanic Beendet Programm mit Fehlercode Pflicht-Ressourcen in der Initialisierung
calloc(n, size) 0 bei OOM Wenn Nullinitialisierung gewünscht

Alignment

std.alloc garantiert 8-Byte-Alignment für alle Allokationen — ausreichend für int64, f64 und Zeiger. Für SIMD-Operationen (16/32 Byte) muss manuell ausgerichtet werden:

import std.alloc;

fn AllocAligned(size: int64, alignment: int64): int64 {
    // Überschuss allokieren und auf Grenze ausrichten
    var raw: int64 := malloc(size + alignment);
    if (raw == 0) { return 0; }
    var aligned: int64 := (raw + alignment - 1) & -(alignment);
    return aligned;
    // Achtung: raw muss gespeichert werden, um es später korrekt freizugeben
}


5. Fat Pointers — Arrays mit Längeninformation

Dynamische Arrays in Lyx sind Fat Pointers: Ein 24-Byte-Struct aus Basisadresse, aktueller Länge (len) und Kapazität (cap). Der Zeiger zeigt auf den Heap-Puffer, aber Länge und Kapazität werden mitgeführt.

┌─────────────────────────────────────────────┐
│ Fat Pointer (24 Byte auf dem Stack)         │
│  base: int64 ──→ [Heap: Elemente]          │
│  len:  int64     [0][1][2][3]...[n-1]      │
│  cap:  int64                                │
└─────────────────────────────────────────────┘

Das macht Array-Zugriffe sicher: Lyx kann zur Laufzeit prüfen, ob ein Index in [0, len) liegt — ohne dass die Länge separat mitgegeben werden muss.

fn main(): int64 {
    // Statisches Array: kein Fat Pointer — liegt vollständig auf dem Stack
    var stack_arr: int64[8] := [1, 2, 3, 4, 5, 6, 7, 8];

    // Dynamisches Array: Fat Pointer — Heap-Puffer, Länge und Kapazität
    var dyn_arr: int64[] := new int64[](4);   // len=0, cap=4, Heap allokiert

    // Elemente hinzufügen (wächst automatisch bei Überschreitung der Kapazität)
    dyn_arr.push(10);
    dyn_arr.push(20);
    dyn_arr.push(30);

    PrintInt(dyn_arr.len);   // 3
    PrintInt(dyn_arr.cap);   // 4

    dyn_arr.push(40);
    dyn_arr.push(50);        // cap überschritten → realloc auf cap=8

    PrintInt(dyn_arr.len);   // 5
    PrintInt(dyn_arr.cap);   // 8 (verdoppelt)

    // Freigabe: gibt den Heap-Puffer frei, Fat Pointer selbst ist auf dem Stack
    dispose dyn_arr;

    return 0;
}

Bei @redundant werden die drei Felder des Fat Pointers (base, len, cap) dreifach im RAM abgelegt und bei jedem Zugriff per Mehrheitsentscheid ausgelesen:

@flight_crit
fn TrackBuffer(): void {
    @redundant
    var secure_buf: int64[] := new int64[](16);
    // base, len und cap liegen 3× im RAM
    // Ein Bit-Flip beeinflusst das Ergebnis nicht
    secure_buf.push(42);
    dispose secure_buf;
}


6. Lebensdauer und Dangling Pointers

Da Lyx keinen GC hat, muss der Entwickler Lebensdauer-Regeln einhalten. Der häufigste Fehler: Ein Zeiger auf Speicher, der bereits freigegeben wurde (Dangling Pointer).

fn GetBuffer(): int64 {
    var local_buf: uint8[256];
    return local_buf as int64;   // FEHLER: Zeiger auf Stack-Speicher
    // local_buf wird freigegeben wenn GetBuffer() zurückkehrt
    // Der zurückgegebene Zeiger ist danach ungültig
}

fn GetHeapBuffer(): int64 {
    var heap_buf: int64 := new uint8[256];
    return heap_buf;   // OK: Heap-Speicher überlebt den Scope
    // Aufrufer ist verantwortlich für dispose
}

Ownership-Konvention

Lyx hat kein eingebautes Ownership-System. Die Konvention ist:

// Erzeugt Puffer — Aufrufer ist verantwortlich für dispose
fn CreatePacket(size: int64): int64 {
    return new uint8[size];
}

// Verarbeitet Puffer — gibt ihn NICHT frei
fn SendPacket(pkt: int64, len: int64): bool {
    return NetworkSend(pkt, len);
}

// Gibt Puffer frei — Aufrufer darf ihn danach nicht mehr nutzen
fn FreePacket(pkt: int64): void {
    dispose pkt;
}

fn main(): int64 {
    var pkt: int64 := CreatePacket(512);
    FillPacketHeader(pkt);
    SendPacket(pkt, 512);
    FreePacket(pkt);
    // pkt ist jetzt ungültig — kein weiterer Zugriff
    return 0;
}


7. Speicher in Safety-kritischem Code

DO-178C und verwandte Standards stellen klare Anforderungen an Speicherverhalten. Lyx erzwingt die wichtigste Regel mit einem Compiler-Fehler: @flight_crit-Funktionen dürfen kein new enthalten.

@flight_crit
fn ControlLoop(state: AircraftState): ControlOutput {
    var buf: f64[32];   // OK: Stack, Größe zur Compile-Zeit bekannt

    // var dyn: int64 := new uint8[64];   // Compiler-Fehler:
    // "heap allocation in @flight_crit function"

    // Alle Berechnungen auf Stack-Variablen
    var pitch: f64 := ComputePitch(state);
    var roll:  f64 := ComputeRoll(state);
    return ControlOutput { pitch: pitch, roll: roll };
}

Initialisierungsphase vs. Regelzyklus

Das zulässige Muster für dynamischen Speicher in Safety-Code: Einmalige Allokation beim Start, danach ausschließlich Stack.

// Globale Zeiger auf vorallokierten Speicher
var g_sensor_buf: int64 := 0;
var g_log_buf:    int64 := 0;

con SENSOR_BUF_SIZE: int64 := 4096;
con LOG_BUF_SIZE:    int64 := 65536;

// Initialisierungsphase — new ist erlaubt
fn SystemInit(): bool {
    g_sensor_buf := new uint8[SENSOR_BUF_SIZE];
    if (g_sensor_buf == 0) { return false; }

    g_log_buf := new uint8[LOG_BUF_SIZE];
    if (g_log_buf == 0) {
        dispose g_sensor_buf;
        return false;
    }
    return true;
}

// Regelzyklus — kein new, nur vorallokierte Puffer
@flight_crit
@stack_limit(1024)
fn RunCycle(sensors: SensorArray): void {
    // g_sensor_buf wurde in SystemInit allokiert — sicherer Zugriff
    CopySensorData(sensors, g_sensor_buf, SENSOR_BUF_SIZE);
    ProcessSensorData(g_sensor_buf, SENSOR_BUF_SIZE);
    LogData(g_log_buf, LOG_BUF_SIZE);
}

fn SystemShutdown(): void {
    dispose g_sensor_buf;
    dispose g_log_buf;
}

fn main(): int64 {
    if (!SystemInit()) {
        PrintStr("Initialisierung fehlgeschlagen\n");
        return 1;
    }

    var sensors: SensorArray := ReadSensors();
    while (true) limit(2147483647) {
        RunCycle(sensors);
        sensors := ReadSensors();
    }

    SystemShutdown();
    return 0;
}


8. Speicher-Diagnose und Analyse

Stack-Verbrauch messen

# Statische Stack-Analyse über den gesamten Call-Graph
lyxc flight_ctrl.lyx --stack-check

# Detaillierter Report mit allen Funktionen und Worst-Case-Pfaden
lyxc flight_ctrl.lyx --stack-check --verbose

Heap-Nutzung zur Laufzeit prüfen

Der Debug-Build des Compilers (mit -gh) verfolgt alle Heap-Allokationen und gibt am Programmende eine Zusammenfassung aus:

lyxc-debug myprogram.lyx -o myprogram_debug
./myprogram_debug

=== Heap Summary ===
Total allocated:   1 048 576 bytes
Total freed:       1 048 576 bytes
Peak usage:          131 072 bytes
Remaining (leaks):         0 bytes
Allocation count:        142

Ein verbleibender Restbetrag (Leaks > 0) zeigt nicht freigegebenen Speicher an.


9. Best Practices

Situation Empfehlung
Lokale Variablen Immer Stack — keine Ausnahme
Arrays fester Größe Stack: var buf: uint8[256]
Arrays variabler Größe Heap: new uint8[n] mit dispose
Daten über Scope-Grenzen Heap — mit klarer Ownership-Konvention
Pflicht-Ressourcen in Initialisierung malloc_orpanic — Programm endet bei OOM
Optionale Ressourcen malloc_safe — Aufrufer prüft auf 0
Safety-Code (DAL-A/B) Nur Stack im Regelzyklus, Heap nur in Init
@flight_crit-Funktionen Kein new — Compiler erzwingt das
Stack-Budget nachweisen @stack_limit(N) + –stack-check
TMR für kritische Zustände @redundant auf Stack-Variablen
Speicherlecks aufspüren Debug-Build mit -gh

std.alloc — Vollständige Funktionsreferenz
Aerospace & Safety — Stack-Limit und TMR im Detail