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];nimmtb.push(3)an,len(b)zählt mit.sizeofauf eine solche Variable liefert weiterhin8— sie ist ein Zeiger auf den Kopf, nicht die Nutzlast.
Bis 1.0.16x wies der Parsernew int64[size]ab (expected (, got [), und ein Indexzugriff auf einarray<T>schrieb ohne Meldung auf nicht allozierten Speicher.
Weiterhin tragfähig für rohe Puffer:allocmitpoke/peekundfree_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 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 |
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.
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
}
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)
Heap-Speicher überlebt den Scope seiner Allokation. Er wird mit new angefordert und muss mit dispose explizit freigegeben werden.
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;
}
newliefert genullten Speicher — auf jedem Ziel (#1848, seit 1.1.12I). Ein frisch angelegtes Objekt hat in allen Feldern0; nachgemessen mit 1.1.14A an einer Klasse mit zweiint64-Feldern. Zuvor galt das nur auf einem Teil der Ziele, was Code, der sich darauf verließ, auf dem anderen still falsch machte.
newbelegt seit 1.1.12C die Objektgröße statt einer ganzen Seite (#1836). Wer viele kleine Objekte anlegt, merkt den Unterschied im Speicherbedarf.
Dieselbe Ursache hatteStrConcat(#1980, behoben in 1.2.4A): ein rohermmapje Aufruf, mindestens eine Seite, nie freigegeben — 100.000 Verkettungen kosteten 400 MB, jetzt 3 MB. → std.string
Zwei Schreibweisen, die es nicht gibt.new int64ohne Klammern ist ein Parse-Fehler — für einen Einzelwert alsonew int64[1](#1506). Und eine Dereferenz-Zuweisung wie(ptr as int64) := 42existiert nicht: Lyx führt keinen Zeigertyp, gelesen und geschrieben wird mitpeek64/poke64. Frühere Fassungen dieser Seite zeigten beides.
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;
}
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.
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) {
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;
}
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 |
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
}
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;
}
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
}
Lyx hat kein eingebautes Ownership-System. Die Konvention ist:
new aufruft, besitzt den SpeicherFree/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;
}
DO-178C und verwandte Standards stellen klare Anforderungen an Speicherverhalten. Lyx erzwingt die wichtigste Regel mit einem Compiler-Fehler: @flight_crit (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;
}
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;
}
Werkzeuge zur Speicheranalyse und -diagnose:
# 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
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.
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 |
→ std.alloc — Vollständige Funktionsreferenz
→ Rohspeicher — alloc/free, peek/poke, Byte-Offset-Muster
→ Handles und Dateideskriptoren — Kernel-fd vs. Userspace-Handle
→ 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).