====== Lyx – Low-Level: Pointer & Inlining ====== Systemprogrammierung – Treiber, Kernel-Module, Echtzeit-Regler, Hardware-Abstraktionen – erfordert direkten Zugriff auf Speicheradressen und präzise Kontrolle über den generierten Maschinencode. Lyx bietet dafür ein vollständiges Pointer-Modell nach Pascal-Vorbild sowie mehrere Performance-Pragmas (''@inline'', ''@no_opt'', ''@parallel'', ''@section''), die das Backend gezielt steuern. Sicherheits-Garantien wie Range-Checks und ''@flight_crit''-Regeln bleiben dabei erhalten: Pointer-Arithmetik ist explizit in ''unsafe''-Blöcke eingekapselt, und in zertifizierungspflichtigen Modulen erzeugt ungesicherter Pointer-Einsatz einen Compiler-Fehler. ===== 1. Pointer – Grundlagen ===== Ein Pointer speichert die Adresse einer anderen Variable im Speicher. Intern wird jeder Pointer als vorzeichenloser ''int64'' (8 Byte) dargestellt – auf allen unterstützten 64-Bit-Plattformen (x86_64, ARM64, RISC-V64). ==== Pointer-Operatoren ==== ^ Operator ^ Bedeutung ^ Beispiel ^ | ''%%^T%%'' | Pointer-**Typ** auf T | ''var p: %%^%%int64'' | | ''%%^x%%'' | Adresse von x (**Address-of**) | ''%%^%%counter'' | | ''%%p^%%'' | **Wert** an Adresse p (Dereferenz) | ''p%%^%% := 42'' | | ''nil'' | Null-Pointer (kein gültiges Ziel) | ''var p: %%^%%int64 := nil'' | > **Syntax-Merkregel:** > Der Hut ''%%^%%'' zeigt **vor** dem Typ auf den Typ (''%%^%%int64'' = Pointer auf int64) und **vor** einer Variable auf deren Adresse (''%%^%%x'' = Adresse von x). **Nach** dem Pointer-Namen dereferenziert er (''p%%^%%'' = Wert an p). ==== Lesen und Schreiben über einen Pointer ==== unit demo; import std.io; fn main(): int64 { var x: int64 := 10; var p: ^int64 := ^x; // p zeigt auf x PrintInt(p^); // Ausgabe: 10 (lesen) p^ := 99; // schreiben über den Pointer PrintInt(x); // Ausgabe: 99 (x wurde geändert) return 0; } ''p^'' und ''x'' benennen denselben Speicherplatz: Eine Zuweisung über ''p^'' verändert ''x'' direkt. ===== 2. Pointer auf Structs ===== Struct-Felder sind über einen Pointer mit ''.'' erreichbar – Lyx erfordert keine gesonderte Pfeil-Syntax (''->''). Der Dereferenz-Operator wirkt auf den gesamten Struct-Wert. unit structs; import std.io; type Vec2 = struct { x: f64; y: f64; }; fn Scale(p: ^Vec2, factor: f64) { p^.x := p^.x * factor; p^.y := p^.y * factor; } fn main(): int64 { var v := Vec2 { x: 3.0, y: 4.0 }; Scale(^v, 2.0); PrintFloat(v.x); // 6.0 PrintFloat(v.y); // 8.0 return 0; } Das Übergeben eines Struct-Pointers (''%%^Vec2%%'') ist die Standard-Methode, um große Strukturen ohne Kopier-Overhead zu modifizieren. ===== 3. Mehrfach-Pointer (Pointer auf Pointer) ===== Pointer können auf andere Pointer zeigen. Der Typ ergibt sich durch Verschachtelung mit Klammern. var x: int64 := 42; var p: ^int64 := ^x; // Pointer auf int64 var pp: ^(^int64) := ^p; // Pointer auf Pointer auf int64 PrintInt(p^); // 42 PrintInt(pp^^); // 42 (doppelte Dereferenz) pp^^ := 99; // schreibt 99 direkt in x PrintInt(x); // 99 Mehrfach-Pointer werden in der Praxis für Output-Parameter von Funktionen oder für dynamische Treiber-Dispatch-Tabellen verwendet. ===== 4. nil – Der Null-Pointer ===== ''nil'' ist der Null-Pointer: ein Pointer ohne gültiges Ziel. Der Versuch, ''nil^'' zu lesen oder zu schreiben, führt zu einem Laufzeit-Fehler (Segmentation Fault oder explizitem ''panic''). fn TryRead(p: ^int64): int64 { if (p = nil) { return -1; // Fehlerfall explizit behandeln } return p^; } > **nil-Check-Pflicht:** > Der Compiler warnt (''--lint''), wenn ein Pointer ohne vorherigen nil-Check dereferenziert wird und nicht aus einem Kontext stammt, der nil ausschließt (z. B. ''%%^%%lokaleVariable''). ==== Nullable Pointer-Typen ==== Mit dem ''?''-Suffix wird ein Pointer explizit als nullable deklariert. In Kombination mit dem Null-Coalescing-Operator ''??'' lassen sich Fallback-Werte definieren: var name: pchar? := GetOptionalName(); // darf nil sein var display: pchar := name ?? "Unbekannt"; PrintStr(display); ===== 5. Pointer ↔ int64 – Typ-Casts ===== Da alle Pointer intern als ''int64'' gespeichert sind, ist der explizite Cast in beide Richtungen möglich. Das ist für MMIO-Adressen und FFI-Aufrufe notwendig. var addr: int64 := 0x40020000; var reg: ^uint32 := addr as ^uint32; // Integer → Pointer var p: ^uint8 := get_buffer(); var raw: int64 := p as int64; // Pointer → Integer (für Arithmetik) > **Achtung:** > ''as''-Casts zwischen Pointer und Integer umgehen die Typsicherheit vollständig. Außerhalb von ''unsafe''-Blöcken sind sie nur für MMIO-Adressen (''@volatile'') und nachgewiesene FFI-Szenarien gedacht. ===== 6. @volatile – Memory-Mapped I/O (MMIO) ===== Auf Mikrocontrollern (ESP32, STM32, RISC-V) sind Hardware-Register direkt in den Adressraum eingeblendet. Ohne ''@volatile'' kann der Compiler Zugriffe auf dieselbe Adresse wegoptimieren, weil er keine Seiteneffekte erkennt. ==== GPIO-Beispiel (ESP32 / RISC-V) ==== unit gpio; @volatile var GPIO_OUT: ^uint32 := 0x3FF44004 as ^uint32; // Setze GPIOs HIGH @volatile var GPIO_OUT_W1TC: ^uint32 := 0x3FF4400C as ^uint32; // Lösche GPIOs (Write-1-to-Clear) fn ToggleLed(pin: uint32) { GPIO_OUT^ := 1u32 << pin; // LED an // … Wartezeit … GPIO_OUT_W1TC^ := 1u32 << pin; // LED aus } ==== UART Status-Register ==== @volatile var UART_SR: ^uint8 := 0x40013800 as ^uint8; con UART_TXE: uint8 := 0x80u8; // Transmit Empty Bit fn WaitTxReady() { while (UART_SR^ & UART_TXE = 0) { } // Busy-Wait – darf nicht wegoptimiert werden } ''@volatile'' garantiert, dass jeder Lese- und Schreibzugriff exakt in der programmierten Reihenfolge im Maschinencode erscheint. ===== 7. pchar – String als Zeiger ===== ''pchar'' ist ein Alias für ''%%^%%uint8'' (''char*'' in C) und repräsentiert einen nullterminierten ASCII-String. String-Literale in Lyx haben den Typ ''pchar''. let msg: pchar := "Hallo Lyx"; PrintStr(msg); // gibt "Hallo Lyx" aus // Einzelne Zeichen lesen: var first: uint8 := msg^; // 'H' = 72 var second: uint8 := (msg + 1)^; // 'a' = 97 (unsafe!) Das Lesen einzelner Zeichen per Offset-Arithmetik erfordert einen ''unsafe''-Block (→ Abschnitt 8). Für sicheres String-Handling stehen ''std.string'' und ''std.io'' zur Verfügung. ^ Situation ^ Empfehlung ^ | Read-only Ausgabe | ''pchar''-Literal direkt an ''PrintStr'' / FFI übergeben | | Zeichen lesen | ''std.string.CharAt(s, i)'' statt manueller Arithmetik | | String manipulieren | ''std.string''-Funktionen; pchar nur für FFI-Grenzen | | Ownership übergeben | Kommentar, ob Aufrufer oder Aufgerufener free() trägt | ===== 8. unsafe – Pointer-Arithmetik ===== Pointer-Arithmetik (Offset-Addition/Subtraktion) ist in Lyx standardmäßig verboten. Innerhalb eines ''unsafe''-Blocks hebt der Compiler diese Einschränkung auf und erlaubt skalierte Adress-Berechnung. unit buffer_ops; import std.io; fn ReadWord(base: ^uint8, offset: int64): uint32 { var result: uint32; unsafe { var ptr: ^uint8 := base + offset; // Pointer-Arithmetik var p32: ^uint32 := ptr as ^uint32; // Reinterpret-Cast result := p32^; // Lese 4 Bytes } return result; } ==== Was ist in unsafe erlaubt? ==== ^ Operation ^ Beschreibung ^ | ''ptr + n'' / ''ptr - n'' | Addition/Subtraktion um n Bytes (skaliert nach Elementgröße) | | ''ptr as ^OtherType'' | Reinterpret-Cast: gleiche Adresse, anderer Typ | | ''(ptr)^'' | Dereferenz eines berechneten Zeigers | | ''addr as ^T'' | Integer → Pointer (MMIO, FFI) | ==== Typisches Muster: Byte-Buffer ==== fn CopyBytes(dst: ^uint8, src: ^uint8, count: int64) { var i: int64 := 0; while (i < count) { unsafe { (dst + i)^ := (src + i)^; } i++; } } > **Sicherheits-Warnung:** > ''unsafe''-Blöcke erscheinen im Compiler-Report (''--call-graph'') und in der Provenance-Ausgabe (''--provenance''). In Modulen mit ''@dal(A)'' oder ''@dal(B)'' erzeugt ''unsafe'' standardmäßig einen **Compiler-Fehler**, sofern keine explizite Ausnahme mit ''@allow_unsafe'' dokumentiert ist. ===== 9. Pointer in Safety-Code ===== Lyx unterscheidet klar zwischen zulässigen und verbotenen Pointer-Operationen in sicherheitskritischen Modulen. ^ Operation ^ @flight_crit ^ @dal(A) ^ @dal(B/C) ^ | Pointer auf Stack-Variable (''%%^%%x'') | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt | | Pointer als Funktionsparameter (''%%^%%T'') | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt | | ''@volatile'' MMIO-Zugriff | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt | | ''nil''-Pointer (mit Prüfung) | ✅ Erlaubt | ✅ Erlaubt | ✅ Erlaubt | | ''new'' / ''dispose'' (Heap) | ❌ Verboten | ❌ Verboten | ⚠️ Warnung | | ''unsafe'' Pointer-Arithmetik | ❌ Compiler-Fehler | ❌ Compiler-Fehler | ⚠️ Warnung | | ''as''-Cast Pointer ↔ int64 | ⚠️ Warnung | ❌ Compiler-Fehler | ⚠️ Warnung | ==== Empfohlenes Muster für @flight_crit ==== Alle Puffer und Arbeitsspeicher werden in der **Init-Phase** als Stack-Arrays angelegt und als Pointer weitergereicht. Innerhalb des Regel-Zyklus entstehen keine neuen Pointer und keine Arithmetik. @flight_crit @dal(B) @stack_limit(4096) unit sensor_ctrl; fn main(): int64 { // Init-Phase: Puffer als Stack-Array var samples: [256]f64; var buf_ptr: ^f64 := ^samples[0]; // Pointer auf erstes Element – erlaubt // Regel-Zyklus: nur sicherer Zugriff while (true) { var val := ReadSensor(); buf_ptr^ := val; // Schreiben via Pointer – erlaubt buf_ptr^ := ProcessSample(buf_ptr^); } return 0; } ===== 10. @inline – Inlining erzwingen ===== ''@inline'' weist den Compiler an, den Funktionsaufruf durch den Funktionskörper zu ersetzen. Damit entfällt der Aufruf-Overhead (Register-Sicherung, Stack-Frame-Aufbau, Branch). ==== Wann sinnvoll? ==== * Kleine mathematische Hilfsfunktionen (''clamp'', ''min'', ''max'', Bit-Operationen) * Getter/Setter für Felder (Zugriff mit Null-Overhead) * Heiße Pfade in Schleifen, die weniger als ~10 Instruktionen erzeugen @inline fn clamp(val: int64, lo: int64, hi: int64): int64 { if (val < lo) { return lo; } if (val > hi) { return hi; } return val; } @inline fn abs_f64(x: f64): f64 { if (x < 0.0) { return -x; } return x; } fn main(): int64 { var speed: int64 := clamp(ReadSensor(), 0, 300); // kein Funktionsaufruf im Asm return 0; } ==== Trade-off: Geschwindigkeit vs. Code-Größe ==== ^ Aspekt ^ Ohne @inline ^ Mit @inline ^ | Aufruf-Overhead | Ja (push/pop, call/ret) | Nein | | Code-Größe | Klein (eine Kopie) | Wächst pro Aufrufstelle | | Instruction-Cache | Gut (dichte Loop) | Schlechter bei vielen Aufrufstellen | | Stack-Tiefe | Tiefer (Stack-Frame) | Flacher | > **Energy-Level-Interaktion:** > Das Energy-Aware-Backend inlinet bei Level 4–5 automatisch kleine Funktionen auch ohne ''@inline''. Bei Level 1–2 wird Inlining bewusst reduziert, um den Instruction-Cache zu schonen. ''@inline'' **erzwingt** Inlining unabhängig vom Energy-Level. ===== 11. @no_opt – Optimierung deaktivieren ===== ''@no_opt'' verhindert jede Optimierung für eine einzelne Funktion. Das ist notwendig, wenn: * Timing exakt eingehalten werden muss (Busy-Wait-Schleifen) * Der Debugger den Code 1:1 nachvollziehen muss * Eine Hardware-Sequenz in genau dieser Reihenfolge ablaufen muss @no_opt fn WaitMicros(us: int64) { var i: int64 := 0; while (i < us * 100) { // Schleife bleibt erhalten – kein "dead loop" Entfernen i++; } } @no_opt fn SendByte(b: uint8) { while (TX_SR^ & 0x80u8 = 0) { } // Warte auf "TX empty" TX_DATA^ := b; // Sende Byte } Im Gegensatz zu ''@volatile'' (das nur Lade-/Store-Reihenfolge schützt) verhindert ''@no_opt'' auch das Umordnen, Fusionieren oder Entfernen beliebiger Operationen. ===== 12. @parallel – SIMD-Vektorisierung ===== ''@parallel'' signalisiert dem Compiler, dass eine Schleife oder ein Array-Block keine Datenabhängigkeiten zwischen den Iterationen besitzt und für SIMD-Vektorisierung geeignet ist (AVX2 auf x86_64, NEON auf ARM64). unit dsp; fn NormalizeSignal(samples: [1024]f32, gain: f32) { @parallel for i := 0 to 1023 do { samples[i] := samples[i] * gain; } } fn DotProduct(a: [512]f64, b: [512]f64): f64 { var sum: f64 := 0.0; @parallel for i := 0 to 511 do { sum := sum + a[i] * b[i]; } return sum; } ^ Plattform ^ SIMD-Erweiterung ^ Vektorgröße ^ | x86_64 (AVX2) | 256-Bit-Register | 8× f32 oder 4× f64 | | ARM64 (NEON) | 128-Bit-Register | 4× f32 oder 2× f64 | | RISC-V (V-Extension) | Variabel (>=128 Bit) | Abhängig vom Hart | > **Voraussetzung:** > Der Compiler prüft, ob Schreibzugriffe innerhalb der Schleife unabhängig voneinander sind. Falls Aliasing möglich ist (z. B. ''src'' und ''dst'' zeigen auf überlappende Bereiche), warnt ''--lint'' und verzichtet auf Vektorisierung. ===== 13. @section – ELF-Sektion steuern ===== ''@section'' platziert den Maschinencode einer Funktion oder die Daten einer Variablen in einer benannten ELF-Sektion. Das ist entscheidend für: * **Embedded-Systeme**: Code in schnellem SRAM statt langsamem Flash * **Bootloader**: Init-Code in einer separaten Sektion (''".boot"'') * **Interrupt-Handler**: ISR-Code muss im direkt erreichbaren Speicher liegen @section(".fast_ram") fn IsrUart() { // Wird in SRAM geladen – kein Flash-Read-Wait-State var byte := UART_DATA^; RingBufferPush(byte); } @section(".slow_mem") fn PrintDiagnostics() { // Seltener Aufruf – darf im langsamen Flash liegen PrintStr("Diagnostics OK"); } @section(".boot") fn SystemInit() { ClockSetup(); MemoryInit(); } Die Sektions-Namen müssen im Linker-Script (''*.ld'') definiert sein und einer physikalischen Speicherregion zugeordnet werden. ===== 14. Vollständiges Beispiel: Hardware-Treiber mit Pointer & Pragmas ===== Ein UART-Treiber kombiniert alle beschriebenen Mechanismen: unit uart_driver; import std.io; // ── Hardware-Register (MMIO) ────────────────────────────────────────────────── @volatile var UART_DR: ^uint8 := 0x40013800 as ^uint8; // Data Register @volatile var UART_SR: ^uint32 := 0x40013804 as ^uint32; // Status Register @volatile var UART_BRR: ^uint32 := 0x40013808 as ^uint32; // Baud Rate con UART_SR_TXE: uint32 := 0x0080u32; // Transmit Empty con UART_SR_RXNE: uint32 := 0x0020u32; // Receive Not Empty // ── Inline-Hilfsfunktionen ──────────────────────────────────────────────────── @inline fn TxReady(): bool { return UART_SR^ & UART_SR_TXE != 0; } @inline fn RxReady(): bool { return UART_SR^ & UART_SR_RXNE != 0; } // ── Busy-Wait (darf nicht wegoptimiert werden) ──────────────────────────────── @no_opt fn WaitTx() { while (!TxReady()) { } } // ── Byte senden ─────────────────────────────────────────────────────────────── @section(".fast_ram") @flight_crit fn SendByte(b: uint8) { WaitTx(); UART_DR^ := b; } // ── String senden ───────────────────────────────────────────────────────────── fn SendStr(s: pchar) { var p: ^uint8 := s as ^uint8; while (p^ != 0u8) { SendByte(p^); unsafe { p := p + 1; } } } // ── Buffer empfangen ───────────────────────────────────────────────────────── fn RecvBytes(dst: ^uint8, count: int64): int64 { var received: int64 := 0; while (received < count) { if (RxReady()) { unsafe { (dst + received)^ := UART_DR^ as uint8; } received++; } } return received; } fn main(): int64 { UART_BRR^ := 0x683u32; // 9600 Baud @ 16 MHz SendStr("UART bereit\n"); return 0; } ===== 15. Zusammenfassung: Wann was nutzen? ===== ^ Feature ^ Einsatzgebiet ^ Risiko / Einschränkung ^ | ''%%^T%%'' Stack-Pointer | Struct-Parameter, Output-Werte, Treiber-Register | nil-Check notwendig | | ''@volatile'' | Hardware-Register (MMIO), Shared Memory Multicore | Keines – verhindert Fehloptimierung | | ''nil''-Check | Pointer aus externen Quellen, optionale Werte | Pflicht vor Dereferenz | | ''unsafe'' + Arithmetik | Byte-Buffer, pchar-Iteration, FFI-Wrapper | Verboten in @dal(A/B) | | ''pchar'' | String-Literale, C-FFI-Strings | Keine Bounds-Checks – std.string bevorzugen | | ''@inline'' | Kleine Helferfunktionen, heiße Schleifen | Code-Größe steigt | | ''@no_opt'' | Busy-Wait, Hardware-Sequenzen, Timing-kritisch | Kein Optimierungs-Benefit | | ''@parallel'' | DSP/ML-Schleifen ohne Datenabhängigkeit | Aliasing-Check notwendig | | ''@section'' | ISR, Bootloader, SRAM-Placement | Linker-Script muss passen | **Weiterführende Seiten:** * [[lyx_-_programmiersprache:memory-management|Memory Management – Stack, Heap, std.alloc]] * [[lyx_-_programmiersprache:ffi|FFI – C-Interoperabilität & @extern]] * [[lyx_-_programmiersprache:das-energy-aware-programmiermodell|Energy-Aware Programmiermodell]] * [[lyx_-_programmiersprache:aerospace-safety|Aerospace & Safety – @flight_crit & DO-178C]] * [[lyx_-_programmiersprache:attributes-pragmas|Attribute & Pragmas – vollständige @-Referenz]]