====== Lyx – Überblick ======
Lyx ist eine kompakte, statisch typisierte, prozedurale Sprache mit einem nativen Compiler (FreePascal), der direkt ausführbare Binaries erzeugt. Der Fokus liegt zunächst auf Linux x86_64 (ELF64), weil das den kleinsten Reibungsverlust für einen „echten“ Bootstrap bietet (stabile ABI, klare ELF-Spezifikation, gutes Tooling).
Lyx setzt bewusst auf einen kleinen, vorhersehbaren Kern: einfacher Kontrollfluss, klarer Typcheck, ein transparentes Speicher-/Laufzeitmodell (primär Stack, keine implizite Heap- oder GC-Magie). Die Runtime bleibt minimal (Builtins/Syscalls) — externe Bibliotheken sind optional und werden bei Bedarf über Dynamic ELF (PLT/GOT) angebunden, ohne dass “libc” zur Pflicht wird.
Wichtig ist aber nicht „x86_64 um jeden Preis“, sondern die saubere Entkopplung: Sprache/Frontend bleiben targetneutral; Targets werden über klar definierte Verträge angebunden (Frontend → IR → Backend → ELF-Writer). Dadurch kann Lyx später auf weitere ISAs (z. B. ARM64) und andere Output-Formate (z. B. Mach-O, PE) erweitert werden, ohne dass jede Sprachänderung eine Neuschreibung des Codegens erzwingt.
Kurz: heute ELF/x86_64, morgen andere Targets — ohne dass du den Compiler jedes Mal „auf links“ drehst. (Trade-off: erst funktionale Stabilität/Bootstrap, Optimierungen und Komfortfeatures danach.)
**Repository:** [[https://github.com/SEOLizer/Lyx]]
----
===== TL;DR =====
* Output: Native ELF64 Executables für Linux x86_64 (ohne externen Linker)
* Runtime: bewusst minimal — Builtins + direkte Syscalls, kein libc-Zwang
* Architektur: klare Entkopplung der Pipeline: Frontend → IR → Backend → ELF-Writer (targetneutral im Frontend)
* Stand v0.1.4: Module/Imports, Dynamic ELF (PLT/GOT), extern fn inkl. Varargs, erste stdlib-Units (u. a. std.math)
* Known Issue: Cross-Unit Function Calls linken korrekt, schlagen zur Laufzeit jedoch fehl („Execution NOK“) — Ursache aktuell Backend-Flagging externer Symbole
----
===== Ziele und Leitplanken =====
Dieser Abschnitt definiert keine Vision oder Marketing-Roadmap, sondern technische Leitplanken für die Weiterentwicklung von Lyx. Ziel ist es, mit wachsender Featureliste keine strukturelle Komplexitätsschuld aufzubauen.
Neue Sprachfeatures müssen sich in die bestehende Architektur einfügen, statt sie zu umgehen. Insbesondere dürfen weder Frontend noch IR durch targetspezifische Sonderfälle erodieren. Erweiterungen gelten nur dann als sinnvoll, wenn sie das bestehende Modell vereinfachen oder klar erweitern — nicht, wenn sie kurzfristig Komfort schaffen, aber langfristig den Codegen oder die Semantik destabilisieren.
Die folgenden Prinzipien dienen als Guardrails:
* Architektur vor Feature-Druck: Sprache, IR und Backend bleiben strikt getrennt.
* Bootstrap-Fähigkeit vor Perfektion: lauffähige, nachvollziehbare Binaries haben Priorität vor aggressiven Optimierungen.
* Vorhersehbarkeit statt Magie: Speicher-, Typ- und Laufzeitverhalten müssen explizit und debugbar bleiben.
* Minimale Runtime: keine versteckten Abhängigkeiten, kein impliziter libc-Zwang.
* Evolvierbarkeit: Änderungen am Sprachkern dürfen nicht jedes Target neu brechen.
Diese Leitplanken sollen sicherstellen, dass Lyx auch bei wachsendem Umfang technisch konsistent bleibt und nicht zu einer Sammlung von Spezialfällen degeneriert.
==== 1. Minimaler Kern statt Feature-Explosion ====
Lyx priorisiert einen kleinen, nachvollziehbaren Sprachkern:
prozedural + Funktionen
statisches Typensystem
klar definierter Kontrollfluss (if, while, return)
Neue Features müssen sich in das bestehende IR‑ und Backend‑Modell einfügen. Wenn ein Feature nur durch Spezialfälle im Codegen funktioniert, ist es kein Kandidat für den Sprachkern.
==== 2. Strikte Trennung von Sprache und Zielplattform ====
Die Sprache selbst kennt keine:
Register-Namen
Syscall-Nummern
ELF- oder ABI-Details
Alles Targetspezifische lebt ausschließlich im Backend. Dadurch bleibt das Frontend stabil, selbst wenn neue Architekturen (z. B. ARM64) hinzukommen.
==== 3. IR als Stabilitätsanker ====
Das interne IR ist die einzige Schicht, die sowohl Sprache als auch Backend verbindet:
AST darf kein Maschinencode kennen
Backend darf keine AST-Knoten benötigen
Diese Entkopplung verhindert, dass jede Sprachänderung eine komplette Neuschreibung des Emitters erzwingt.
==== 4. Bootstrap vor Perfektion ====
Lyx verfolgt bewusst eine inkrementelle Strategie:
zuerst lauffähige ELF-Binaries ohne libc
danach bessere ABI‑Integration und Dynamic Linking
Optimierungen erst nach funktionaler Stabilität
Ziel ist ein Compiler, der früh reale Programme ausführen kann, statt ein theoretisch perfektes Design ohne ausführbares Ergebnis.
==== 5. Vorhersehbares Speicher- und Laufzeitmodell ====
Für frühe Versionen gelten einfache Regeln:
Stackbasierte lokale Variablen
keine implizite Heapverwaltung
Builtins statt komplexer Runtime
Das reduziert Komplexität im Backend und hält Debugging transparent.
----
==== Warum Linux x86_64 zuerst? ====
Weil es die **geringste Reibung** für einen echten, nativen Bootstrap liefert: stabile ABI, klare ELF-Spezifikation, gute Tooling-Landschaft (`readelf`, `objdump`).
Die relevante Entscheidung ist aber nicht „x86_64“, sondern: **keine Vermischung von Sprache und Target**. Kein Syscall-Nummern-Kram im Frontend, keine Register-Namen in der Semantik.
==== Architekturprinzip====
Das Intermediate Representation (IR) bildet den Stabilitätsanker der gesamten Compiler-Pipeline. Es trennt die sprachliche Semantik konsequent von der Zielarchitektur und verhindert, dass Backend-Details in Parser oder Semantik einsickern.
AST-Knoten repräsentieren ausschließlich die Sprachebene; das Backend arbeitet ausschließlich auf IR-Instruktionen. Zwischen beiden Schichten existiert ein klar definierter Contract. Dadurch bleiben Sprachänderungen lokal im Frontend und IR-Lowering — ohne dass der Codegenerator jedes Mal neu gedacht werden muss.
Sobald ein Compiler beginnt, AST direkt in x86-Bytes zu übersetzen, entsteht eine harte Kopplung zwischen Syntax, Semantik und Target. Jede neue Sprachfunktion würde dann Änderungen im gesamten Backend erzwingen. Das IR verhindert genau diesen Effekt: Es stabilisiert die Pipeline, ermöglicht neue Targets und hält die Architektur langfristig wartbar.
----
===== Architektur =====
----
==== Schichtenmodell ====
* **Frontend**
* Lexer
* Parser → AST
* Semantik (Scopes, Typen, Mutability-Regeln)
* **Middle-End**
* AST → **IR** (targetunabhängig)
* optionale Mini-Optimierungen (constant folding, dead code trivial)
* **Backend**
* IR → Machine-IR/Assembler-ähnlich (Instruktionen + Labels)
* Target: x86_64 (Encoding, Register, Calling Conv)
* Output: ELF64 Writer (Header/Segments/Relocs)
==== Contracts====
* **IR-Contract (targetunabhängig):** z. B. `ConstInt`, `Add`, `Call`, `Br`, `Cmp`, später `Load/Store`, optional SSA/Phi.
* **ISA-Contract (Target-API):** z. B. `emitMovRegImm`, `emitLabel`, `emitJmp`, `emitSyscall`.
* **Output-Contract (ELF64):** Code-Blob, Data-Blob, Entry-Offset, Segment-Flags (+ bei Dynamic ELF Relocations/Symtabs).
----
===== Sprache (Kern) =====
----
==== Paradigma und Syntax ====
Lyx folgt einem bewusst reduzierten, prozeduralen Paradigma mit klar definierten Funktionen. Der Sprachkern bleibt absichtlich überschaubar, damit Semantik, IR und Backend stabil bleiben und sich der Compiler schrittweise entwickeln kann. Abstraktionen werden nur dann ergänzt, wenn sie sich ohne Sonderlogik in das bestehende Modell integrieren lassen.
Blöcke werden mit { … } strukturiert. Diese Blocksyntax definiert Scope-Grenzen eindeutig und sorgt für ein vorhersehbares Lifetime-Modell lokaler Variablen.
Zuweisungen verwenden bewusst := statt =. Dadurch bleibt der Unterschied zwischen Vergleich (==) und Wertzuweisung syntaktisch eindeutig, und der Parser vermeidet Mehrdeutigkeiten in frühen Sprachversionen.
[[lyx_-_programmiersprache:syntax|Lyx - Syntax]]
==== Minimalbeispiele ====
**Hello:**
fn main(): int64 {
print_str("Hello Lyx\n");
return 0;
}
**while + compile-time Konstante:**
con LIMIT: int64 := 5;
fn main(): int64 {
var i: int64 := 0;
while (i < LIMIT) {
print_int(i);
print_str("\n");
i := i + 1;
}
return 0;
}
----
===== Typen und Regeln =====
==== Primitive Typen (Stand: v0.1.4 Spec-Kern) ====
* Signed: `int8`, `int16`, `int32`, `int64`
* Unsigned: `uint8`, `uint16`, `uint32`, `uint64`
* Floats: `f32`, `f64`
* `bool`, `void`, `pchar`, `array`
**Wichtige Semantikregeln:**
* `if`/`while`-Bedingungen müssen `bool` sein
* Vergleiche liefern `bool`
* `&&`/`||` sind short-circuit
**Hinweis zur Spec-Konsistenz (bitte fixen):**
In einer Stelle steht „Unsigned Prefix `uin`“, die Typnamen sind aber `uint*`. Entscheide dich konsistent (Empfehlung: **`uint*`**, weil verbreitet und schon im Text genutzt). Sonst wird’s ein dauerhaftes Bug-Fossil.
[[lyx_-_programmiersprache:datentypen|Lyx - Datentypen]]
----
===== Variablen und Konstanten =====
----
==== Storage-Klassen ====
* `var` – mutable (lokaler Stackslot)
* `let` – immutable nach Init (lokaler Stackslot)
* `co` – readonly runtime-constant (lokaler Stackslot; semantisch wie `let`, aber reserviert)
* `con` – compile-time constant (Top-Level; kein lokaler Slot)
**Kurzvergleich:**
^ Keyword ^ Änderbar ^ Compilezeit bekannt ^ Speicher ^
| `var` | ja | nein | Stack |
| `let` | nein | nein | Stack |
| `co` | nein | optional | Stack |
| `con` | nein | ja | immediate / rodata |
----
===== Builtins und Runtime =====
Builtins sind bewusst als Compiler-Spezialfälle implementiert und benötigen kein klassisches Linking gegen externe Libraries. Sie bilden eine minimale, kontrollierte Runtime-Schicht, die direkt über Syscalls bzw. eingebettete Routinen umgesetzt wird.
Aktuell verfügbare Basis-Builtins:
* exit(code: int64): void - Beendet das Programm und übergibt den Exit-Code an das Betriebssystem.
* print_str(s: pchar): void - Gibt eine nullterminierte Bytefolge aus (\0 markiert das Ende).
* print_int(x: int64): void - Gibt eine Ganzzahl als Dezimalwert aus (interne itoa-Routine + write-Syscall).
Ziel dieser Builtins ist ein möglichst kleiner Bootstrap-Footprint: Ein minimales, lauffähiges Programm benötigt lediglich print_str und exit. Weitere Komfortfunktionen können später über Standard-Units oder externe Libraries ergänzt werden, ohne den Kern-Compiler aufzublähen.
----
===== Module System (v0.1.4) =====
Lyx unterstützt Units und Imports (inkl. Aliases und selektiven Imports – je nach aktuellem Stand):
* `unit a.b.c;`
* `import std.math;`
* `import std.io as io;`
* `import std.io { print_str as ps, exit };`
**Design-Intention:** kein globaler Namespace – Zugriff auf fremde Symbole nur über Import/Qualifier.
----
===== Extern + Dynamic ELF (v0.1.4) =====
Lyx kann externe Funktionen deklarieren und – falls externe Symbole genutzt werden – automatisch ein **dynamisches ELF** erzeugen (PT_INTERP + PLT/GOT + Relocations).
**Extern (inkl. Varargs):**
extern fn printf(format: pchar, ...): int64;
fn main(): int64 {
printf("count=%d\n", 42);
return 0;
}
**Tooling zum Prüfen:**
* `readelf -l ` (PT_INTERP / Segmente)
* `readelf -d ` (NEEDED / DYNAMIC)
----
===== Backend und ABI =====
----
==== Linux x86_64 SysV (für Funktionscalls) ====
Das Backend ist die einzige Schicht, die target- und ABI-spezifische Details kennen darf. Es übersetzt das targetneutrale IR in eine instruktionale, assemblernahe Form und emittiert daraus Maschinencode (aktuell: Linux x86_64). ABI-Regeln (Calling Convention, Registerbelegung, Stack-Alignment) werden dabei strikt im Backend umgesetzt – nicht im Frontend und nicht im IR.
Aktueller Fokus: Linux x86_64 SysV ABI
* Return-Wert (int64) in RAX
* Argumente 1..6 in RDI, RSI, RDX, RCX, R8, R9
* Stack-Alignment: 16-Byte aligned vor call
Diese klare Trennung sorgt dafür, dass neue Targets (z. B. ARM64) oder neue Output-Formate (z. B. Mach-O/PE) über ein neues Backend angebunden werden können, ohne Semantik oder Sprachdesign neu zu verkabeln.
----
==== Program Entry ====
Der Compiler erzeugt automatisch einen Einstiegspunkt _start, der als Low-Level-Entry für das erzeugte ELF-Binary dient. Diese Routine kapselt ABI-Details und sorgt für einen klar definierten Übergang zwischen Betriebssystem und Lyx-Programm.
Ablauf:
_start initialisiert den minimalen Laufzeitkontext.
Anschließend wird main() aufgerufen (oder – im Syscall-only Modus – direkt eine Runtime-Sequenz ausgeführt).
Der Rückgabewert von main() wird in RAX erwartet und anschließend an exit(rax) übergeben, wodurch der Prozess mit dem entsprechenden Exit-Code beendet wird.
Durch diesen festen Einstiegspunkt bleibt die Sprache selbst frei von plattformspezifischen Startlogiken, während das Backend die ABI-konforme Initialisierung übernimmt.
----
===== Projektstruktur =====
lyxc/
lyxc.lpr
frontend/
lexer.pas
parser.pas
ast.pas
sema.pas
ir/
ir.pas
lower_ast_to_ir.pas
backend/
backend_intf.pas
x86_64/
x86_64_emit.pas
x86_64_sysv.pas
elf/
elf64_writer.pas
util/
diag.pas
bytes.pas
----
===== Status, Versionen und Roadmap =====
----
==== Aktueller Status (v0.1.4) ====
* ✅ Module System + Import/Export
* ✅ Cross-Unit Symbol Resolution
* ✅ Standard Library: `std/math.lyx` (`abs64`, `min64`, `max64`, `times_two`)
* ✅ Dynamic ELF (Interpreter + PLT/GOT)
* ✅ `extern fn` inkl. Varargs (`...`)
* ✅ Relocations (`.rela.plt`, `R_X86_64_JUMP_SLOT`)
* ✅ automatische Static/Dynamic-Auswahl
**Known Issue:** Cross-Unit Function Call Backend-Bug (Link OK, Execution NOK). Ziel v0.1.5: Fix `IsExternalSymbol()` / Backend-Flagging.
==== Roadmap (komprimiert) ====
* **v0.1.5**
* Fix Cross-Unit Call Bug
* IR-Lowering für `for` (Parser vorhanden)
* Integer-Width Backend (int8/uint32 etc.)
* verschachtelte Unary-Ops (`--x`, `!!y`)
* **v0.2**
* Advanced Module Features (Selective Imports, Private Symbols)
* stdlib Ausbau (`std.io`, `std.string`, `std.mem`)
* ABI/Calling Convention weiter härten
* **v1**
* Diagnostics „erwachsen“ (Spans, Notes, Fixits)
* optional Objectfiles + Linker-Ansteuerung
----
===== FAQ (kurz, aber nützlich) =====
==== Warum Builtins statt „normaler“ stdlib Calls? ====
Weil das den Bootstrap radikal vereinfacht: kein Linker, keine libc-Abhängigkeit, klare Semantik. Später kann das Feature-Set über extern/dynamic ELF erweitert werden.
==== Warum `co` neben `let`? ====
`co` ist eine **Design-Reserve**: runtime-constant, aber nicht zwingend compile-time evaluierbar. Heute ist es semantisch nah an `let`, später kann es strengere oder andere Regeln bekommen, ohne `let` zu brechen.
----
===== Mitmachen / Debugging =====
Für Beiträge sind besonders wertvoll:
* Repro-Cases für den Cross-Unit Call Bug
* Tests für Parser/Sema (negative Fälle!)
* ELF/Dynamic-ELF Validierung per `readelf`/`objdump`
----
===== Lizenz =====