====== 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.
> **Heap-Arrays tragen seit lyxc 1.0.17C** (#1255, #1355). ''%%var a := new int64[n];%%'' belegt zur Laufzeit und ist les- und schreibbar; ''var b: array<int64> := [1,2];'' nimmt ''b.push(3)'' an, ''len(b)'' zählt mit. ''sizeof'' auf eine solche Variable liefert weiterhin ''8'' — sie ist ein Zeiger auf den Kopf, nicht die Nutzlast.
>
> Bis 1.0.16x wies der Parser ''%%new int64[size]%%'' ab (''expected (, got [''), und ein Indexzugriff auf ein ''array<T>'' schrieb ohne Meldung auf nicht allozierten Speicher.
>
> Weiterhin tragfähig für rohe Puffer: ''alloc'' mit ''poke''/''peek'' und ''free_mem'':
>
>
var n: int64 := 4;
var b: int64 := alloc(n * 8);
poke64(b + 2*8, 42);
PrintLn(IntToStr(peek64(b + 2*8))); // 42
free_mem(b);
>
> Für feste Größen bleibt das Stack-Array: ''%%var a: [4]int64 := [1,2,3,4];%%''.
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 |
Welche Speicherklasse (''var'', ''let'', ''co'', ''con'') wo hingehört und wie lange eine Variable lebt, steht unter [[lyx_-_programmiersprache:sprache:variablen|Variablen]].
| **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:**
* Allokation = Stapelzeiger-Verschiebung — eine einzige Assembler-Instruktion
* Keine Fragmentierung
* Deterministische Freigabe am Scope-Ende
* WCET-berechenbar — der Speicherverbrauch ist statisch bekannt
**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. Der Compiler analysiert den gesamten Aufrufgraphen statisch — **bei jeder Übersetzung, ohne Flag**. Ein ''--stack-check'' gibt es nicht.
@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 -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: new braucht immer eine Elementzahl
var ptr: int64 := new int64[1];
poke64(ptr, 42); // Wert schreiben
PrintLn(IntToStr(peek64(ptr))); // Wert lesen -> 42
dispose ptr; // Freigabe — danach ist ptr ungültig
return 0;
}
> **''new'' liefert genullten Speicher — auf jedem Ziel** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1848|#1848]], seit 1.1.12I). Ein frisch angelegtes Objekt hat in allen Feldern ''0''; nachgemessen mit 1.1.14A an einer Klasse mit zwei ''int64''-Feldern. Zuvor galt das nur auf einem Teil der Ziele, was Code, der sich darauf verließ, auf dem anderen still falsch machte.
>
> **''new'' belegt seit 1.1.12C die Objektgröße statt einer ganzen Seite** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1836|#1836]]). Wer viele kleine Objekte anlegt, merkt den Unterschied im Speicherbedarf.
>
> **Dieselbe Ursache hatte ''StrConcat''** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1980|#1980]], behoben in 1.2.4A): ein roher ''mmap'' je Aufruf, mindestens eine Seite, nie freigegeben — 100.000 Verkettungen kosteten 400 MB, jetzt 3 MB. → [[lyx_-_programmiersprache:units:string|std.string]]
> **Zwei Schreibweisen, die es nicht gibt.** ''new int64'' ohne Klammern ist ein Parse-Fehler — für einen Einzelwert also ''new int64[1]'' (#1506). Und eine Dereferenz-Zuweisung wie ''(ptr as int64) := 42'' existiert nicht: Lyx führt keinen Zeigertyp, gelesen und geschrieben wird mit ''peek64''/''poke64''. Frühere Fassungen dieser Seite zeigten beides.
==== 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 [[lyx_-_programmiersprache:units|Standardbibliothek]].
import std.alloc;
fn main(): int64 {
// malloc: Speicher allokieren (unkinitialisiert)
var ptr: int64 := malloc(1024);
if (ptr == 0) {
PrintLn("Out of Memory");
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) {
PrintLn("Kein Speicher — Datensatz übersprungen");
return;
}
// ... verarbeiten ...
free_mem(buf);
}
fn InitSystemBuffers(): void {
// malloc_orpanic wiederholt den Versuch, bis er gelingt — es kehrt also
// entweder mit gültigem Zeiger zurück oder gar nicht.
var ring_buf: int64 := malloc_orpanic(65536);
// ring_buf ist hier gültig
}
fn InitSystemBuffersMitAbbruch(): void {
// Sauberer Abbruch statt Warteschleife: selbst prüfen und panic() rufen
var ring_buf: int64 := malloc(65536);
if (ring_buf == 0) { panic("kein Speicher fuer den Ringpuffer"); }
}
^ 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'' | **Wiederholt den Versuch endlos** — kehrt nie mit 0 zurück, hängt aber bei echter Speichernot | nur wenn Warten sinnvoll ist; für Pflicht-Ressourcen besser ''malloc'' + ''panic()'' |
| ''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[]; // len=0, waechst bei push
// Elemente hinzufügen (wächst automatisch bei Überschreitung der Kapazität)
dyn_arr.push(10);
dyn_arr.push(20);
dyn_arr.push(30);
PrintLn(IntToStr(dyn_arr.len)); // 3
PrintLn(IntToStr(dyn_arr.cap)); // 4
dyn_arr.push(40);
dyn_arr.push(50); // cap überschritten → realloc auf cap=8
PrintLn(IntToStr(dyn_arr.len)); // 5
PrintLn(IntToStr(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:
* Wer ''new'' aufruft, **besitzt** den Speicher
* Wer Besitz übergibt (z.B. als Rückgabewert), kommentiert das klar
* Funktionen, die einen Zeiger empfangen, geben ihn normalerweise **nicht** frei
* Ausnahme: Funktionen mit ''Free''/''Close''/''Destroy'' im Namen
// 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'' ([[lyx_-_programmiersprache:sprache:attributes-pragmas|Attributes & Pragmas]])-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);
var _r: ControlOutput;
_r.pitch := pitch;
_r.roll := roll;
return _r;
}
==== 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: int64): void { // Zeiger auf den Sensorblock
// 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()) {
PrintLn("Initialisierung fehlgeschlagen");
return 1;
}
var sensors: int64 := ReadSensors(); // Zeiger auf den Sensorblock
while (true) limit(2147483647) {
RunCycle(sensors);
sensors := ReadSensors();
}
SystemShutdown();
return 0;
}
----
===== 8. Speicher-Diagnose und Analyse =====
Werkzeuge zur Speicheranalyse und -diagnose:
==== Stack-Verbrauch messen ====
# Statische Stack-Analyse über den gesamten Call-Graph
lyxc flight_ctrl.lyx -o flight_ctrl
# Detaillierter Report mit allen Funktionen und Worst-Case-Pfaden
lyxc flight_ctrl.lyx --static-analysis -o flight_ctrl
==== Heap-Nutzung zur Laufzeit prüfen ====
''lyxc'' hat kein eingebautes Heap-Tracking für kompilierte Programme. Für Leak-Analyse empfiehlt sich **valgrind** auf Linux:
lyxc myprogram.lyx --debug -o myprogram_debug
valgrind --leak-check=full ./myprogram_debug
==3842== HEAP SUMMARY:
==3842== in use at exit: 0 bytes in 0 blocks
==3842== total heap usage: 142 allocs, 142 frees, 1,048,576 bytes allocated
==3842==
==3842== All heap blocks were freed -- no leaks are possible
Verbleibende Blöcke (''definitely lost'') zeigen vergessene ''free()''-Aufrufe an. Das ''--debug''-Flag ist für valgrind nicht zwingend, liefert aber Zeilennummern im Leak-Report.
----
===== 9. Best Practices =====
Empfehlungen für den produktiven Einsatz:
^ 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'' + Prüfung + ''panic()'' — ''malloc_orpanic'' wartet stattdessen endlos |
| 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)'' — wird ohne Flag bei jeder Übersetzung geprüft |
| TMR für kritische Zustände | ''@redundant'' auf Stack-Variablen |
| Speicherlecks aufspüren | ''--debug'' + ''valgrind --leak-check=full'' |
→ [[lyx_-_programmiersprache:units:alloc|std.alloc — Vollständige Funktionsreferenz]]\\
→ [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher — alloc/free, peek/poke, Byte-Offset-Muster]]\\
→ [[lyx_-_programmiersprache:sprache:handles-und-fd|Handles und Dateideskriptoren — Kernel-fd vs. Userspace-Handle]]\\
→ [[lyx_-_programmiersprache:guides:aerospace-safety|Aerospace & Safety — Stack-Limit und TMR im Detail]]
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).