Lyx – Objektorientierte Programmierung

Lyx unterscheidet bewusst zwischen zwei Arten von benutzerdefinierten Typen: Structs und Klassen. Diese Trennung ist kein Stilmittel — sie macht die Performance-Implikationen im Code sichtbar. Ein Struct liegt auf dem Stack, ein Klassenobjekt auf dem Heap. Ein Struct-Aufruf ist ein direkter Funktionsaufruf, ein virtueller Methodenaufruf geht durch eine V-Table. Beides ist in Lyx explizit und damit für den Entwickler kontrollierbar.

 
PrintF64 ist seit lyxc 1.0.17C ein Builtin (#1254, #1284) — kein Import nötig, auch nicht std.io. Es gibt sechs Nachkommastellen mit abschließendem Zeilenumbruch aus: PrintF64(1.25)1.250000. Die Ausgabe rundet sichtbar (3.141593.141589); für exakte Stellenzahl FloatToStr nehmen.

Ebenfalls in std.io: PrintFloatLn(x: f64) (mit Umbruch) und FloatToStr(val: f64, prec: int64) (Zeichenkette, etwa Print(FloatToStr(3.75, 2)) für Ausgabe ohne Umbruch).
Eigenschaft Struct Klasse
Speicherort Stack (Copy-by-Value) Heap (Reference-Type)
Vererbung Nein Ja (extends)
Virtuelle Methoden Nein Ja (virtual / override)
Allokation Automatisch (Scope) Explizit (new / dispose)
Polymorphismus Nein Ja (V-Table)
WCET-Berechenbarkeit Exakt Eingeschränkt (V-Table-Dispatch)
Einsatz Sensordaten, Vektoren, Pakete Treiber, Komponenten, Protokoll-Handler

1. Structs

Structs sind stackbasierte, leichtgewichtige Datencontainer. Zuweisung kopiert den Inhalt vollständig (Copy-by-Value) — es gibt keinen versteckten Heap-Zugriff.

Definition und Felder

type Point = struct {
    x: int64;
    y: int64;
};

type SensorReading = struct {
    sensor_id:   int64;
    temperature: f64;
    pressure:    f64;
    timestamp:   int64;
    valid:       bool;
};

Methoden im Struct

Structs können Methoden besitzen. self referenziert die aktuelle Instanz. Da Structs Value-Types sind, muss var self verwendet werden, wenn eine Methode Felder verändert:

unit main;
import std.io;
import std.string;
import std.math;
import std.alloc;

type Point = struct {
    x: int64;
    y: int64;

    // Lesende Methode — self wird nicht verändert
    fn LengthSquared(): int64 {
        return self.x * self.x + self.y * self.y;
    }

    fn DistanceTo(other: Point): f64 {
        var dx: int64 := other.x - self.x;
        var dy: int64 := other.y - self.y;
        return SqrtF64((dx * dx + dy * dy) as f64);
    }

    // Mutierende Methode — verändert self
    fn Translate(dx: int64, dy: int64): void {
        self.x := self.x + dx;
        self.y := self.y + dy;
    }

    fn ToString(): pchar {
        // StrFormat nimmt immer fuenf Argumente — ungenutzte mit 0 auffuellen
        return StrFormat("(%d, %d)", self.x, self.y, 0, 0, 0);
    }
};

fn main(): int64 {
    var p1: Point;
    p1.x := 3;
    p1.y := 4;
    var p2: Point;
    p2.x := 0;
    p2.y := 0;

    Print("Länge²: ");
    PrintLn(IntToStr(p1.LengthSquared()));  // 25

    Print("Abstand: ");
    PrintF64(p1.DistanceTo(p2));    // 5.0
    PrintLn("");

    p1.Translate(1, 1);
    PrintLn(p1.ToString());        // (4, 5);

    // Copy-by-Value: p3 ist eine unabhängige Kopie von p1
    var p3: Point := p1;
    p3.Translate(10, 10);
    // p1 ist unverändert
    PrintLn(p1.ToString());        // (4, 5);

    return 0;
}

Structs in Safety-Code

Structs sind die bevorzugte Datenstruktur in @flight_crit-Funktionen (Attributes & Pragmas): deterministisch, keine Allokation, WCET-exakt berechenbar.

type FlightState = struct {
    altitude:  f64;
    airspeed:  f64;
    heading:   f64;
    bank:      f64;
    pitch:     f64;
    valid:     bool;
};

type ControlCommand = struct {
    throttle:  int64;
    aileron:   int64;
    elevator:  int64;
};

@flight_crit
@stack_limit(512)
fn ComputeCommand(state: FlightState): ControlCommand {
    if (!state.valid) {
        var _r: ControlCommand;
        _r.throttle := 50;
        _r.aileron := 0;
        _r.elevator := 0;
        return _r;
    }
    var _r: ControlCommand;
    _r.throttle := ClampI(50 + (250 - state.airspeed as int64) / 5, 0, 100);
    _r.aileron := ClampI(-(state.bank as int64), -45, 45);
    _r.elevator := ClampI((10000 - state.altitude as int64) / 50, -30, 30);
    return _r;
}


2. Klassen

Klassen sind Reference-Types: Sie werden mit new auf dem Heap angelegt und mit dispose freigegeben. Eine Klasse unterstützt Vererbung und virtuelle Methoden.

Definition

type Logger = class {
    log_fd:  int64;
    prefix:  pchar;
    count:   int64;

    // Konstruktor — wird von new aufgerufen
    fn Create(fd: int64, pfx: pchar): void {
        self.log_fd := fd;
        self.prefix := pfx;
        self.count  := 0;
    }

    // Aufräum-Methode — muss ausdrücklich gerufen werden, siehe Kasten unten
    fn Destroy(): void {
        if (self.log_fd > 2) {
            SysClose(self.log_fd);
        }
    }

    pub fn Log(msg: pchar): void {
        self.count := self.count + 1;
        PrintLn(StrConcat(StrConcat(self.prefix, ": "), msg));
    }

    pub fn Count(): int64 {
        return self.count;
    }
};

fn main(): int64 {
    var logger: Logger := new Logger(1, "[APP]");

    logger.Log("Programm gestartet");
    logger.Log("Verbindung hergestellt");

    PrintLn(StrConcat("Log-Einträge: ", IntToStr(logger.Count())));

    dispose logger;    // ruft Destroy und gibt den Heap-Speicher frei
    return 0;
}

 
Eine Variable vom Klassentyp ohne Zuweisung ist null. Das Lazy-Init-Muster (if (g == null) { g := new T(…); }) funktioniert damit auch für globale Variablen; bis lyxc 1.1.1C war der Vergleich dort immer falsch, weil die Globale auf ihren eigenen Speicher gepatcht wurde (#1619).

0 ist als Nullwert überall erlaubt — bei Zuweisung, Initialisierung, Rückgabe und Vergleich, in Methoden wie in freien Funktionen (#1629). Ein anderer ganzzahliger Wert (var a: T := 7;) bleibt ein Typfehler.

Verkettete Aufrufe über eine globale Variable (g.Item(0).SetS("x"c)) erreichen seit #1631 die Codeerzeugung; vorher endeten sie in no codegen implementation found.
 
dispose ruft Destroy nur bei einer typisierten Variablen. var logger: Logger := new Logger(…) räumt beim dispose auf; var logger: int64 := new Logger(…) gibt nur den Speicher frei — dem Compiler fehlt dort die Typangabe, an der die Aufräum-Methode hängt. Frühere Fassungen dieser Seite zeigten die int64-Form; wer sie noch verwendet, ruft Destroy vor dem dispose ausdrücklich auf.

Mit der typisierten Variablen werden Methoden zudem direkt aufgerufen (logger.Log(…)) statt über den Umweg (logger as Logger).Log(…).
(Issue #1235)
 
Der Cast als Empfänger arbeitet wieder (#1588 behoben, mit 1.1.2E nachgemessen). (obj as C).feld liefert den Feldwert, (obj as C).Methode() ruft die Methode; vorher gab der Feldzugriff still 0 zurück und der Methodenaufruf brach mit no codegen implementation found ab.

Die typisierte Variable (var o: C := obj as C;) bleibt trotzdem die lesbarere Form — und die einzige, an der dispose den Destruktor findet.

Sichtbarkeit

Ohne explizite Angabe sind Felder und Methoden unit-privat — außerhalb der Unit nicht sichtbar. pub macht sie öffentlich.

type Database = class {
    pub host: pchar;      // öffentlich — andere Units können lesen/schreiben
    pub port: int64;

    socket_fd: int64;     // privat — nur innerhalb dieser Unit zugänglich
    connected: bool;

    pub fn Connect(): bool {
        self.socket_fd := OpenTCPSocket(self.host, self.port);
        self.connected := self.socket_fd > 0;
        return self.connected;
    }

    pub fn IsConnected(): bool {
        return self.connected;
    }

    // Privat — nur intern genutzt
    fn ResetState(): void {
        self.socket_fd := -1;
        self.connected := false;
    }

    pub fn Disconnect(): void {
        if (self.connected) {
            SysClose(self.socket_fd);
            self.ResetState();
        }
    }
};

static — Methoden ohne Instanz

Eine mit static markierte Methode gehört zur Klasse, nicht zu einem Objekt. Sie hat kein self und wird über den Klassennamen aufgerufen:

type Mathe = class {
    static fn Quadrat(x: int64): int64 { return x * x; }
};

fn main(): int64 {
    PrintLn(IntToStr(Mathe.Quadrat(7)));   // 49 — keine Instanz nötig
    return 0;
}

Typischer Einsatz: Hilfsfunktionen, die thematisch zur Klasse gehören, aber keinen Objektzustand brauchen — etwa Fabrikmethoden oder Umrechnungen.

Statische Felder gibt es nicht. static v: int64; wird mit statische Felder werden nicht unterstuetzt abgewiesen. Für klassenweite Werte eine con-Konstante auf Unit-Ebene verwenden.

3. Vererbung

Eine Klasse erbt Felder und Methoden ihrer Elternklasse mit extends. In Lyx gibt es einfache Vererbung (eine Elternklasse). Mehrere Interfaces werden über Traits modelliert.

// Basisklasse
type Shape = class {
    color: int64;   // RGB als int64

    fn Create(c: int64): void {
        self.color := c;
    }

    virtual fn Area(): f64 {
        return 0.0;
    }

    virtual fn Perimeter(): f64 {
        return 0.0;
    }

    pub fn Describe(): void {
        Print(StrConcat(StrConcat("Farbe: ", IntToStr(self.color)), ", Fläche: "));
        PrintF64(self.Area());
        PrintLn(" cm²");
    }
};

// Abgeleitete Klasse
con PI: f64 := 3.14159265358979;   // std kennt keine PI-Konstante

type Circle = class extends Shape {
    radius: f64;

    fn Create(c: int64, r: f64): void {
        super.Create(c);     // Eltern-Konstruktor aufrufen
        self.radius := r;
    }

    override fn Area(): f64 {
        return PI * self.radius * self.radius;
    }

    override fn Perimeter(): f64 {
        return 2.0 * PI * self.radius;
    }
};

type Rectangle = class extends Shape {
    width:  f64;
    height: f64;

    fn Create(c: int64, w: f64, h: f64): void {
        super.Create(c);
        self.width  := w;
        self.height := h;
    }

    override fn Area(): f64 {
        return self.width * self.height;
    }

    override fn Perimeter(): f64 {
        return 2.0 * (self.width + self.height);
    }
};

fn main(): int64 {
    var circle: int64 := new Circle(0xFF0000, 5.0);
    var rect:   int64 := new Rectangle(0x00FF00, 4.0, 6.0);

    (circle as Circle).Describe();
    (rect   as Rectangle).Describe();

    dispose circle;
    dispose rect;
    return 0;
}

Create und Destroy werden vererbt

Definiert eine Ableitung keinen eigenen Konstruktor oder Destruktor, gilt der der Elternklasse — genau wie bei gewöhnlichen Methoden. Bis lyxc 1.1.1C unterblieb der Aufruf still, ohne Warnung (#1624).

type Basis = class {
    v: int64;
    fn Create(x: int64): void { self.v := x; PrintLn("Basis.Create"); }
    fn Destroy(): void { PrintLn("Basis.Destroy"); }
    pub fn Zeig(): void { PrintLn(StrConcat("v = ", IntToStr(self.v))); }
};

type Ablt = class extends Basis {
    w: int64;                      // kein eigener Create/Destroy
};

fn main(): int64 {
    var a: Ablt := new Ablt(7);    // ruft Basis.Create
    a.Zeig();
    dispose a;                     // ruft Basis.Destroy
    return 0;
}

Basis.Create
v = 7
Basis.Destroy

Die Stelligkeit eines geerbten Konstruktors wird dabei geprüft: new Ablt() ohne Argument wird abgewiesen.

Ein eigener Konstruktor muss super.Create() rufen

Sobald eine Ableitung ein eigenes Create schreibt, wird der Konstruktor der Basis nicht mehr von selbst ausgeführt — er muss ausdrücklich gerufen werden. Seit lyxc 1.2.4A ist das keine Empfehlung mehr, sondern eine Regel, die der Compiler durchsetzt (#1973):

type Ablt = class extends Basis {
    w: int64;
    fn Create(x: int64, y: int64): void {
        super.Create(x);           // Pflicht — sonst Übersetzungsfehler
        self.w := y;
    }
};

Fehlt der Aufruf, bricht die Übersetzung ab und nennt die betroffene Klasse. Vorher blieben die geerbten Felder still auf null — ein Zustand, den man erst bemerkt, wenn weit später eine Rechnung nicht aufgeht.

 
Die Regel gilt ohne Ausnahme. Es gibt keinen Schalter und kein Attribut, um sie abzuschalten — eine solche Hintertür würde genau den stillen Zustand zurückholen, den die Prüfung abschafft. Wer einen tragfähigen Gegenfall hat, meldet ihn als Issue.

Drei Richtungen sind geprüft: ein geerbtes Create läuft (kein eigener Konstruktor, nichts zu rufen), ein ausdrücklich gerufenes läuft, ein fehlendes wird gemeldet — und eine Basisklasse ohne Create verlangt nichts.

Zur Verschärfung selbst: keine einzige der 17 Klassen mit Vererbung in der Standardbibliothek verstößt gegen die Regel. Sie trifft also nur echte Fälle, nicht gewachsenen Bestand.

Inline-Arrays in Ableitungen

Ein Feld fester Größe in der Basisklasse belegt seinen vollen Platz; das erste eigene Feld der Ableitung liegt dahinter. Bis lyxc 1.1.1C rechnete das Layout mit acht Byte pro Basisfeld, ein [64]int64 der Basis wurde deshalb vom ersten Feld der Ableitung überschrieben (#1630).

 
Geerbte Felder tragen auch über Unit-Grenzen (#1985, behoben in 1.2.4A). Lag die Basisklasse in einer anderen Unit, ließ _fieldOffsetIn den Versatz auf 0: die eigenen Felder der Ableitung begannen wieder bei null und überlagerten die geerbten — gelesen wurde also nicht 0, sondern ein fremdes Feld. Dazu zählte _typeSizeOf die Basisfelder nicht mit, das Objekt war zu klein.

Beide laufen jetzt im Registry-Raum weiter, wie _resolveMethodIdx seit #1976. Denselben Weg nahm #1998: eine geerbte nicht-virtuelle Methode lieferte 0, sobald die Basisklasse irgendeine virtual-Methode hatte — auch das nur über die Modulgrenze hinweg. Nachgemessen mit zwei geerbten Feldern, damit ein Versatz um eine Position auffällt: 44 22 5 77 22 5 — gleich auf x86-64, arm64 und riscv.

Ein Inline-Array von Klassenreferenzen ([N]TKlasse) hält Referenzen, keine Kopien: was hineingeschrieben wurde, kommt identisch wieder heraus, und ein Schreibzugriff über das gelesene Objekt erreicht das Original. Bis lyxc 1.1.2H legte die Zuweisung eine Kopie ab — back == t war dann false (#1646).

pub type TThing = class { Value: int64; fn Create(): void { self.Value := 0; } }

pub type THolder = class {
    Slots: [4]TThing;
    Count: int64;
    fn Create(): void { self.Count := 0; }
    fn Add(t: TThing): void { self.Slots[self.Count] := t; self.Count := self.Count + 1; }
    fn At(i: int64): TThing { return self.Slots[i]; }
}

fn main(): int64 {
    var t: TThing := new TThing();
    t.Value := 1;
    var h: THolder := new THolder();
    h.Add(t);
    var back: TThing := h.At(0);
    PrintLn(back == t);        // 1 — dasselbe Objekt
    back.Value := 42;
    PrintLn(t.Value);          // 42 — der Schreibzugriff trifft das Original
    return 0;
}

1
42

super — Elternmethoden aufrufen

super.MethodName() ruft die nächste Implementierung entlang der Vererbungskette auf — nicht nur die der unmittelbaren Basisklasse. Überspringt eine Zwischenklasse die Methode, greift super eine Ebene höher; bis lyxc 1.1.3B brach der Codegen dort mit undefined function ab (#1650). Das ist besonders im Konstruktor und beim partiellen Überschreiben nützlich:

type TimestampedLogger = class extends Logger {
    start_time: int64;

    fn Create(fd: int64, pfx: pchar): void {
        super.Create(fd, pfx);          // Logger.Create aufrufen
        self.start_time := SysTimeMs();
    }

    override pub fn Log(msg: pchar): void {
        var elapsed: int64 := SysTimeMs() - self.start_time;
        Print(StrConcat(StrConcat("[", IntToStr(elapsed)), "ms] "));
        super.Log(msg);                 // Logger.Log aufrufen
    }
};


4. Virtuelle Methoden und Polymorphismus

Virtuelle Methoden ermöglichen Polymorphismus: Verschiedene Klassen können dieselbe Basisklasse implementieren und austauschbar verwendet werden. Der Aufruf wird zur Laufzeit über eine V-Table aufgelöst.

type Sensor = class {
    id: int64;

    fn Create(sensor_id: int64): void {
        self.id := sensor_id;
    }

    virtual fn Read(): f64 {
        return 0.0;
    }

    virtual fn IsReady(): bool {
        return true;
    }

    virtual fn Name(): pchar {
        return "Sensor";
    }
};

type PressureSensor = class extends Sensor {
    port: int64;

    fn Create(sid: int64, hw_port: int64): void {
        super.Create(sid);
        self.port := hw_port;
    }

    override fn Read(): f64 {
        var raw: int64 := MemRead32(self.port);
        return (raw as f64) * 0.1;    // Rohwert → hPa
    }

    override fn Name(): pchar { return "PressureSensor"; }
};

type TemperatureSensor = class extends Sensor {
    port: int64;

    fn Create(sid: int64, hw_port: int64): void {
        super.Create(sid);
        self.port := hw_port;
    }

    override fn Read(): f64 {
        var raw: int64 := MemRead32(self.port);
        return (raw as f64) * 0.0625 - 40.0;   // Rohwert → °C
    }

    override fn Name(): pchar { return "TemperatureSensor"; }
};

// Diese Funktion arbeitet mit jedem Sensor — Polymorphismus
fn LogSensor(sensor: int64): void {
    var s: Sensor := sensor as Sensor;
    if (!s.IsReady()) {
        PrintLn("Sensor nicht bereit");
        return;
    }
    Print(StrConcat(s.Name(), " → "));
    PrintF64(s.Read());
    PrintLn("");
}

fn main(): int64 {
    var pressure: int64    := new PressureSensor(1, 0x40020000);
    var temperature: int64 := new TemperatureSensor(2, 0x40020004);

    // Beide Aufrufe gehen durch LogSensor — V-Table entscheidet welche Read()
    LogSensor(pressure);
    LogSensor(temperature);

    dispose pressure;
    dispose temperature;
    return 0;
}

Wie die V-Table funktioniert

Jede Klasse mit virtuellen Methoden bekommt eine Virtual Table (V-Table): ein Array von Funktionszeigern, einem pro virtueller Methode. Jede Instanz trägt einen Zeiger auf die V-Table ihrer Klasse.

Speicherlayout einer Sensor-Instanz:
┌──────────────────────────────────────────┐
│ vtable_ptr ──→ PressureSensor V-Table   │
│                  [0] Read  → PressureSensor.Read   │
│                  [1] IsReady → Sensor.IsReady      │
│                  [2] Name  → PressureSensor.Name   │
│ id:         int64                        │
│ port:       int64                        │
└──────────────────────────────────────────┘

Ein virtueller Aufruf s.Read() wird zu: lade vtable_ptr → springe zu vtable[0]. Das ist ein indirekter Sprung — minimal mehr Overhead als ein direkter Call, aber der WCET-Analysator kann den Zielbereich nicht statisch einengen.


5. Abstrakte Methoden

Eine abstrakte Methode hat keinen Körper und muss in der abgeleiteten Klasse überschrieben werden.

new auf einer Klasse mit abstrakten Methoden ist erlaubt — der Fehler fällt erst beim Aufruf auf, dafür laut und mit Namen. Bis lyxc 1.1.2H sprang der leere VMT-Eintrag nach 0 und das Programm starb mit einem Speicherzugriffsfehler ohne jeden Hinweis (#1643):

type Animal = class {
    fn Create(): void { }
    abstract fn Speak(): int64;
};

fn main(): int64 {
    var a: Animal := new Animal();
    return a.Speak();          // bricht definiert ab
}

lyxc: abstrakte Methode Animal.Speak ohne Implementierung aufgerufen

type Transport = class {
    name: pchar;

    fn Create(n: pchar): void {
        self.name := n;
    }

    // Abstrakt — muss implementiert werden
    abstract fn Send(data: int64, len: int64): int64;
    abstract fn Recv(buf: int64, max_len: int64): int64;
    abstract fn IsConnected(): bool;

    // Konkrete Methode — nutzt die abstrakten
    pub fn SendString(msg: pchar): bool {
        if (!self.IsConnected()) { return false; }
        var len: int64 := StrLen(msg);
        return self.Send(msg as int64, len) == len;
    }
};

type TCPTransport = class extends Transport {
    fd: int64;

    fn Create(host: pchar, port: int64): void {
        super.Create("TCP");
        self.fd := OpenTCPSocket(host, port);
    }

    override fn Send(data: int64, len: int64): int64 {
        return SysWrite(self.fd, data, len);
    }

    override fn Recv(buf: int64, max_len: int64): int64 {
        return SysRead(self.fd, buf, max_len);
    }

    override fn IsConnected(): bool {
        return self.fd > 0;
    }
};

type UARTTransport = class extends Transport {
    port: int64;

    fn Create(hw_port: int64): void {
        super.Create("UART");
        self.port := hw_port;
    }

    override fn Send(data: int64, len: int64): int64 {
        return UARTWrite(self.port, data, len);
    }

    override fn Recv(buf: int64, max_len: int64): int64 {
        return UARTRead(self.port, buf, max_len);
    }

    override fn IsConnected(): bool {
        return UARTIsReady(self.port);
    }
};


6. Laufzeit-Typprüfung: is und as

is prüft zur Laufzeit, ob ein Objekt eine bestimmte Klasse oder eine ihrer Unterklassen ist. as führt den Cast durch — gibt es einen Typ-Mismatch, löst as einen panic() aus.

fn ProcessSensor(sensor: int64): void {
    var s: Sensor := sensor as Sensor;

    // is: sicherer Typ-Check
    if (sensor is PressureSensor) {
        var ps: PressureSensor := sensor as PressureSensor;
        Print("Druck: ");
        PrintF64(ps.Read());
        PrintLn(" hPa");
    } else if (sensor is TemperatureSensor) {
        var ts: TemperatureSensor := sensor as TemperatureSensor;
        Print("Temperatur: ");
        PrintF64(ts.Read());
        PrintLn(" °C");
    } else {
        // Generischer Fallback
        Print(StrConcat(s.Name(), ": "));
        PrintF64(s.Read());
        PrintLn("");
    }
}

as ohne vorangehendes is ist nur sicher, wenn der Typ durch den Programmfluss garantiert ist:

// Sicher: Der Typ kommt aus einer bekannten Quelle
var known_pressure: int64 := new PressureSensor(1, 0x4000);
var ps: PressureSensor := known_pressure as PressureSensor;  // kein Panic möglich

// Unsicher — nur mit vorherigem is:
fn Cast(unknown: int64): void {
    if (unknown is PressureSensor) {
        var ps: PressureSensor := unknown as PressureSensor;  // OK
    }
    // var ps: PressureSensor := unknown as PressureSensor;   // Panic wenn kein PressureSensor
}


ClassName() — der Klassenname zur Laufzeit

obj.ClassName(): pchar liefert seit lyxc 1.1.3C den Namen der Klasse (#1670). Bei einer Klasse mit Methoden ist das der dynamische Name: eine als Basisklasse deklarierte Variable meldet die tatsächliche Klasse. Eine Klasse ohne Methoden bekommt Struct-Layout und trägt keinen Typzeiger — dort ist der statische Name die richtige und einzige Antwort. Dieselbe Grenze zieht is schon.

import std.io;

pub type TA = class { fn Create(): void { } virtual fn Sag(): void { } }
pub type TB = class extends TA { override fn Sag(): void { } }

fn main(): int64 {
    var b: TB := new TB();
    var a: TA := b;                     // statisch TA, tatsächlich TB
    Print("dyn: "); Print(a.ClassName()); Print("\n");
    return 0;
}

dyn: TB

 
Behoben seit lyxc 1.1.4A (#1683): Print(a.ClassName()) druckt jetzt den Namen. Bis 1.1.3O kam an dieser Stelle der Zeiger als Zahl heraus, weil die Auswahl der Print-Fassung auf int64 zurückfiel; wer noch mit einem älteren Compiler arbeitet, weist das Ergebnis erst einer pchar-Variablen zu.

ClassName() ersetzt kein vollständiges RTTI: Feld- und Methodennamen sind zur Laufzeit weiterhin nicht abfragbar, und der Name der Instanz (Form1.EdName in Delphi) ist eine Sache der jeweiligen Bibliothek, nicht des Compilers.


7. Komposition statt Vererbung

Tiefe Vererbungshierarchien erschweren die WCET-Analyse und machen Code schwerer testbar. Die bevorzugte Alternative in Lyx ist Komposition: Eine Klasse enthält andere Klassen als Felder.

// ── Schlechtes Muster: tiefe Hierarchie ───────────────────────────────────

type Base = class { ... };
type Middle = class extends Base { ... };
type Concrete = class extends Middle { ... };   // V-Table mit 3 Ebenen

// ── Besseres Muster: Komposition ──────────────────────────────────────────

type DataBuffer = struct {
    data: int64;
    len:  int64;
    cap:  int64;
};

type Framer = struct {
    header_size: int64;

    fn WrapData(buf: DataBuffer): DataBuffer { ... }
};

type SerialPort = class {
    baud:   int64;
    fd:     int64;
    framer: Framer;   // Komposition — kein extends nötig
    buffer: DataBuffer;

    fn Create(device: pchar, baud_rate: int64): void {
        self.fd     := OpenSerial(device);
        self.baud   := baud_rate;
        _t: Framer;
        _t.header_size := 4;
        self.framer := _t;
        _t: DataBuffer;
        _t.data := new uint8[4096];
        _t.len := 0;
        _t.cap := 4096;
        self.buffer := _t;
    }

    pub fn Send(data: int64, len: int64): void {
        raw: DataBuffer;
        raw.data := data;
        raw.len  := len;
        raw.cap  := len;
        var framed: DataBuffer := self.framer.WrapData(raw);
        SysWrite(self.fd, framed.data, framed.len);
    }
};


8. Operator-Überladung

Definiert eine Klasse Methoden mit festgelegten Namen, lassen sich die zugehörigen Operatoren direkt auf ihre Instanzen anwenden. Der Compiler übersetzt a + b dann in a.Add(b). Neue Syntax entsteht dabei nicht — es ist eine reine Auflösungsregel.

Operator Methode Operator Methode
+ Add(o) == Eq(o)
- Sub(o) != Ne(o)
* Mul(o) < Lt(o)
/ Div(o) Le(o)
% Mod(o) > Gt(o)
a[i] Get(i) >= Ge(o)

import std.io;

type Vec = class {
    x: int64;
    fn Add(o: Vec): Vec { var r: Vec := new Vec(); r.x := self.x + o.x; return r; }
    fn Eq(o: Vec):  bool { return self.x == o.x; }
    fn Get(i: int64): int64 { return self.x * 100 + i; }
};

fn main(): int64 {
    var a: Vec := new Vec();  a.x := 10;
    var b: Vec := new Vec();  b.x := 2;

    var c: Vec := a + b;              // ruft a.Add(b)
    PrintLn(IntToStr(c.x));           // 12

    if (a == b) { PrintLn("gleich"); } else { PrintLn("verschieden"); }

    PrintLn(IntToStr(a[7]));          // ruft a.Get(7) -> 1007
    return 0;
}

Auflösungsregeln

  • Maßgeblich ist der statische Typ des linken Operanden. Ist er eine Klasse mit der passenden Methode, greift die Überladung; andernfalls behält der Operator seine eingebaute Bedeutung. Ganzzahl-, Gleitkomma- und pchar-Operationen sowie die normale Array-Indizierung bleiben also unberührt.
  • Fehlt Ne, wird != auf die Negation von Eq zurückgeführt. Die übrigen Operatoren haben keinen solchen Rückfall.
  • Als linker Operand taugt auch ein Methodenaufruf oder eine freie Funktion mit Klassenrückgabe. Ketten lösen sich rekursiv auf: a.M() + b, a.M() == b, a.M()[i] und a + b + c funktionieren.
  • Eine Typangabe am Operanden ist seit lyxc 1.0.15E nicht mehr nötig: var a := Mk(10); übernimmt die Klasse aus dem deklarierten Rückgabetyp von Mk, und var c := a + b; trägt die Klasse des linken Operanden. Bleibt die Klasse eines Operanden ausnahmsweise statisch unbestimmbar, behält der Operator vereinbarungsgemäß seine eingebaute Bedeutung.

8a. Was der Compiler in Methodenrümpfen prüft

Bis lyxc 1.1.1C wurde der Rumpf einer Methode gar nicht geprüft, solange weder die Klasse noch eine Methode eine Capability-Annotation trug: ein Tippfehler im Bezeichner wurde still zu 0 ausgewertet, freie Funktionen dagegen wurden seit jeher korrekt abgewiesen (#1622). Jetzt wird immer geprüft; die Annotation entscheidet nur noch, welche Capabilities gelten.

Zwei weitere Lücken an derselben Grenze sind geschlossen:

  • Falsche Argumentzahl bei Methoden importierter Klassen wird gemeldet (#1626) — vorher nur bei freien Funktionen.
  • Der Rückgabetyp einer geerbten Interface-Methode bleibt über die Ableitung erhalten; pchar und bool wurden vorher zu int64 (#1621).
  • Auf den IR-Zielen (lyxos, arm64, riscv, arm-cm4, esp32) gibt es keine VMT. Virtueller Dispatch läuft dort über einen type-id-Switch: obj[0] trägt einen globalen Registry-Index, der gegen jede Unterklasse verglichen wird, dann folgt ein statischer Aufruf. Interfaces lösen seit lyxc 1.1.9G über denselben Switch auf — über alle Klassen, die das Interface führen, direkt oder geerbt, auch aus einer anderen Unit (#1773). Vorher landete der Aufruf in der rumpflosen Methode der Interface-Deklaration und gab zurück, was zufällig im Rückgaberegister stand. Interface-Tabellen und Selektoren braucht dieser Weg nicht — das ist der Weg des x86-Backends, nicht des IR-Pfads.
  • Struct-Parameter über Modulgrenzen werden typgeprüft: ein int64 anstelle eines Structs übersetzte vorher kommentarlos und wurde zur Adresse (#1606).
  • Zwei Mitglieder gleichen Namens in derselben Klasse werden abgewiesen — Lyx kennt keine Überladung, es konnte also ohnehin nur eine Fassung gelten (#1668): sema error (line 2): Methode 'Wert' ist in TDing bereits deklariert (zuerst in Zeile 2).
  • Ein unbekanntes Feld wird auch dann gemeldet, wenn die Basisklasse aus einer anderen Unit stammt (#1666); vorher wurde x.gibtesnicht := 99 dort still angenommen.

Reihenfolge der Deklarationen

Klassen dürfen sich in derselben Unit vorwärts verweisen: eine Methode kann eine Klasse als Parametertyp nehmen, die erst weiter unten deklariert wird. Bis lyxc 1.1.3A übersetzte das fehlerfrei, lieferte aber 0 bei jedem Feldzugriff und brach beim Methodenaufruf ab (#1663).

import std.io;

pub type TA = class {
    n: int64;
    fn Create(): void { self.n := 1; }
    fn Ask(o: TB): int64 { return o.m; }   // TB ist hier noch nicht deklariert
}

pub type TB = class {
    m: int64;
    fn Create(): void { self.m := 7; }
}

fn main(): int64 {
    var a: TA := new TA();
    var b: TB := new TB();
    PrintLn(a.Ask(b));      // 7
    return 0;
}

Vorgabewerte in Methoden

Ein weggelassenes Argument bekommt seinen Vorgabewert auch bei Methoden. Bis lyxc 1.1.2G setzte nur der Weg für freie Funktionen den Wert ein; bei t.M(1) trug b, was zufällig im Register stand (#1637).

import std.io;

type T = class {
    fn Create(): void { }
    fn MitVorgabe(a: int64, b: int64 = 5): int64 { return a + b; }
}

fn main(): int64 {
    var t: T := new T();
    PrintLn(IntToStr(t.MitVorgabe(1)));    // 6
    return 0;
}


9. OOP im Safety-Umfeld

Klassen mit virtuellen Methoden sind in @flight_crit-Funktionen eingeschränkt nutzbar. Virtuelle Aufrufe erzeugen indirekte Sprünge — ihre WCET lässt sich nicht exakt berechnen, wenn der Typ zur Compile-Zeit nicht bekannt ist.

Situation Empfehlung
Daten im Regelzyklus Struct (Stack, deterministisch)
Algorithmen mit festem Typ Direkte Klasseninstanz — kein virtueller Call
Polymorphismus im Regelzyklus Vermeiden — oder Typ zur Compile-Zeit fixieren
Polymorphismus in Initialisierung Unbedenklich — kein WCET-Budget
Tiefe Vererbung (>2 Ebenen) Komposition bevorzugen
Instanziierung mit new Nur in Initialisierungsphase
Typ-Cast mit as Nur nach vorangehendem is-Check

Statischer Dispatch als Alternative

Wo Polymorphismus gewünscht, aber V-Table-Dispatch verboten ist, bieten Generics + Interfaces die Alternative — sie werden durch Monomorphisierung statisch aufgelöst:

// Statt: virtual fn Read(): f64 (V-Table, dynamisch)
// Besser: Generic + Trait (monomorphisiert, statisch)

type SensorHW = interface {
    fn Read(): f64;
    fn IsReady(): bool;
};

@flight_crit
@stack_limit(128)
fn SampleSensor(hw: SensorHW): f64 {
    if (!hw.IsReady()) { return -1.0; }
    return hw.Read();
}

Schranken an Typparametern wirken nicht. Die Form fn SampleSensor&lt;S: SensorHW&gt;(hw: S) wird zwar geparst, ein Methodenaufruf auf dem Typparameter scheitert aber mit undefined function. Der Interface-Typ als Parametertyp — wie oben — erzwingt dieselbe Schnittstelle und wird vom Compiler geprüft. Details: Generics & Interfaces.

10. Vollständiges Beispiel: Plugin-System

Ein Protokoll-Handler-System, das Polymorphismus in der Initialisierung nutzt und im Regelzyklus auf direkten Dispatch umschaltet:

unit ProtocolStack;

// ── Basis-Handler ─────────────────────────────────────────────────────────────

type ProtocolHandler = class {
    name:    pchar;
    enabled: bool;

    fn Create(n: pchar): void {
        self.name    := n;
        self.enabled := true;
    }

    abstract fn HandlePacket(data: int64, len: int64): bool;
    abstract fn GetStats(out_rx: int64, out_tx: int64): void;

    pub fn IsEnabled(): bool { return self.enabled; }
    pub fn Enable():  void  { self.enabled := true; }
    pub fn Disable(): void  { self.enabled := false; }
};

// ── Konkrete Handler ──────────────────────────────────────────────────────────

type HTTPHandler = class extends ProtocolHandler {
    rx_count: int64;
    tx_count: int64;

    fn Create(): void {
        super.Create("HTTP/1.1");
        self.rx_count := 0;
        self.tx_count := 0;
    }

    override fn HandlePacket(data: int64, len: int64): bool {
        if (!self.enabled) { return false; }
        // HTTP-Parsing...
        self.rx_count := self.rx_count + 1;
        return true;
    }

    override fn GetStats(out_rx: int64, out_tx: int64): void {
        (out_rx as int64) := self.rx_count;
        (out_tx as int64) := self.tx_count;
    }
};

type MQTTHandler = class extends ProtocolHandler {
    rx_count: int64;
    tx_count: int64;

    fn Create(): void {
        super.Create("MQTT");
        self.rx_count := 0;
        self.tx_count := 0;
    }

    override fn HandlePacket(data: int64, len: int64): bool {
        if (!self.enabled) { return false; }
        // MQTT-Parsing...
        self.rx_count := self.rx_count + 1;
        return true;
    }

    override fn GetStats(out_rx: int64, out_tx: int64): void {
        (out_rx as int64) := self.rx_count;
        (out_tx as int64) := self.tx_count;
    }
};

// ── Router — nutzt Polymorphismus zur Dispatch-Zeit ───────────────────────────

con MAX_HANDLERS: int64 := 8;

type Router = class {
    handlers: int64[8];   // Array von Klassen-Zeigern
    count:    int64;

    fn Create(): void {
        self.count := 0;
    }

    pub fn Register(handler: int64): bool {
        if (self.count >= MAX_HANDLERS) { return false; }
        self.handlers[self.count] := handler;
        self.count := self.count + 1;
        return true;
    }

    pub fn Dispatch(data: int64, len: int64): int64 {
        var handled: int64 := 0;
        var i: int64 := 0;
        while (i < self.count) limit(MAX_HANDLERS) {
            var h: ProtocolHandler := self.handlers[i] as ProtocolHandler;
            if (h.IsEnabled() && h.HandlePacket(data, len)) {
                handled := handled + 1;
            }
            i := i + 1;
        }
        return handled;
    }

    pub fn Destroy(): void {
        var i: int64 := 0;
        while (i < self.count) limit(MAX_HANDLERS) {
            dispose self.handlers[i];
            i := i + 1;
        }
    }
};

fn main(): int64 {
    // Initialisierungsphase — new ist erlaubt
    var router: int64  := new Router();
    var http:   int64  := new HTTPHandler();
    var mqtt:   int64  := new MQTTHandler();

    (router as Router).Register(http);
    (router as Router).Register(mqtt);

    // Regelzyklus — kein new, nur Dispatch
    var buf: uint8[1500];
    var n: int64 := ReceivePacket(buf as int64, 1500);
    if (n > 0) {
        var handled: int64 := (router as Router).Dispatch(buf as int64, n);
        PrintLn(StrConcat("Handler aktiv: ", IntToStr(handled)));
    }

    // Aufräumen
    (router as Router).Destroy();
    dispose router;
    return 0;
}

Generics & Interfaces
Memory Management — Heap-Allokation und Lebensdauer

Letzte Aktualisierung: 2026-09-08 (#1998, nachgemessen mit lyxc 1.2.5C) — Hinweis auf geerbte nicht-virtuelle Methoden bei virtueller Basis ergänzt.

Zuvor 2026-09-07 (#1973 und #1985, Nachweislauf auf lyxc 1.2.4A) — neuer Abschnitt „Ein eigener Konstruktor muss super.Create() rufen„ (Regel ohne Ausnahme, keine 17 stdlib-Klassen betroffen); Kasten zu geerbten Feldern über Unit-Grenzen ergänzt.

Vorherige letzte Aktualisierung: 2026-08-27 — Dispatch auf den IR-Zielen ergänzt (#1773, 1.1.9G).

Vorherige Aktualisierung: 2026-08-19 — ClassName() (#1670), super entlang der Vererbungskette (#1650), Inline-Arrays von Klassenreferenzen (#1646), lauter Abbruch bei abstrakten Methoden (#1643), doppelte Mitglieder (#1668), Feldprüfung über Unit-Grenzen (#1666), Vorwärtsverweise (#1663) und Vorgabewerte in Methoden (#1637) ergänzt. Alle neuen Beispiele mit lyxc 1.1.3I übersetzt und ausgeführt; ClassName() im Print am 2026-08-20 gegen lyxc 1.1.4A nachgemessen (#1683 behoben).

Vorherige Letzte Aktualisierung: 2026-08-18 — Vererbung von Create/Destroy (#1624), Nullwert-Regeln für Klassenvariablen (#1619, #1629), verkettete Aufrufe über Globale (#1631), Inline-Arrays in Ableitungen (#1630) und die Prüfungen in Methodenrümpfen (#1622, #1626, #1621, #1606) ergänzt. Beispiele mit lyxc 1.1.2E übersetzt und ausgeführt.

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