====== 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).