====== 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.14159'' → ''3.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 ([[lyx_-_programmiersprache:sprache:attributes-pragmas|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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1619|#1619]]).
>
> **''0'' ist als Nullwert überall erlaubt** — bei Zuweisung, Initialisierung, Rückgabe und Vergleich, in Methoden wie in freien Funktionen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1629|#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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1631|#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(…)%%''.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1235|Issue #1235]])
> **Der Cast als Empfänger arbeitet wieder** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1588|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1624|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1973|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1630|#1630]]).
> **Geerbte Felder tragen auch über Unit-Grenzen** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1985|#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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1976|#1976]]. Denselben Weg nahm [[https://github.com/SEOLizer/LyX-Compiler/issues/1998|#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'' ([[https://github.com/SEOLizer/LyX-Compiler/issues/1646|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1650|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1643|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1670|#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** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1683|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1622|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1626|#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'' ([[https://github.com/SEOLizer/LyX-Compiler/issues/1621|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1773|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1606|#1606]]).
* **Zwei Mitglieder gleichen Namens in derselben Klasse** werden abgewiesen — Lyx kennt keine Überladung, es konnte also ohnehin nur eine Fassung gelten ([[https://github.com/SEOLizer/LyX-Compiler/issues/1668|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1666|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1663|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1637|#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<S: SensorHW>(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: [[lyx_-_programmiersprache:sprache:generics-traits|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;
}
→ [[lyx_-_programmiersprache:sprache:generics-traits|Generics & Interfaces]]\\
→ [[lyx_-_programmiersprache:sprache:memory-management|Memory Management — Heap-Allokation und Lebensdauer]]
Letzte Aktualisierung: 2026-09-08 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1998|#1998]], nachgemessen mit lyxc 1.2.5C) — Hinweis auf geerbte nicht-virtuelle Methoden bei virtueller Basis ergänzt.
Zuvor 2026-09-07 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1973|#1973]] und [[https://github.com/SEOLizer/LyX-Compiler/issues/1985|#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()'' ([[https://github.com/SEOLizer/LyX-Compiler/issues/1670|#1670]]), ''super'' entlang der Vererbungskette ([[https://github.com/SEOLizer/LyX-Compiler/issues/1650|#1650]]), Inline-Arrays von Klassenreferenzen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1646|#1646]]), lauter Abbruch bei abstrakten Methoden ([[https://github.com/SEOLizer/LyX-Compiler/issues/1643|#1643]]), doppelte Mitglieder ([[https://github.com/SEOLizer/LyX-Compiler/issues/1668|#1668]]), Feldprüfung über Unit-Grenzen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1666|#1666]]), Vorwärtsverweise ([[https://github.com/SEOLizer/LyX-Compiler/issues/1663|#1663]]) und Vorgabewerte in Methoden ([[https://github.com/SEOLizer/LyX-Compiler/issues/1637|#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'' ([[https://github.com/SEOLizer/LyX-Compiler/issues/1624|#1624]]), Nullwert-Regeln für Klassenvariablen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1619|#1619]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1629|#1629]]), verkettete Aufrufe über Globale ([[https://github.com/SEOLizer/LyX-Compiler/issues/1631|#1631]]), Inline-Arrays in Ableitungen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1630|#1630]]) und die Prüfungen in Methodenrümpfen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1622|#1622]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1626|#1626]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1621|#1621]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1606|#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).