Developing State handling

This commit is contained in:
Michael Schimmel
2025-10-22 13:26:51 +02:00
parent 03465e21d2
commit dd72401ab5
9 changed files with 465 additions and 109 deletions
File diff suppressed because one or more lines are too long
+105
View File
@@ -0,0 +1,105 @@
***
# Projekt-Protokoll: Weiterentwicklung des Sprachdesigns
* **Datum:** 08. Oktober 2025
* **Zeit:** 13:22 CEST
## Motivation
Die bestehende Skriptsprache bietet grundlegende funktionale und imperative Konstrukte, aber die Verwaltung von veränderlichem Zustand (`def` in Verbindung mit `assign`) ist unsicher, insbesondere im Hinblick auf Nebenläufigkeit. Das primäre Motiv für eine Weiterentwicklung ist die Schaffung einer Sprache, die **aus sich selbst heraus sicher ("safe by design")** ist, ohne dabei die Komplexität von Low-Level-Konzepten wie manueller Speicherverwaltung oder einem vollständigen Borrow-Checker (wie in Rust) einzuführen. Die Sprache soll auch für Programmier-Anfänger **intuitiv und leicht verständlich** bleiben.
## Ziel
Das Ziel ist es, ein Sprachdesign zu entwickeln, das eine klare und sichere Handhabung von Zustand erzwingt. Die Kernziele des neuen Designs sind:
1. **Sicherheit durch Design:** Race Conditions durch geteilten, veränderlichen Zustand sollen zur Compile-Zeit erkannt und verhindert werden.
2. **Klare Trennung von Zustandsarten:** Es soll eine unmissverständliche Unterscheidung zwischen temporärem, lokalem Zustand und persistentem, geteiltem Zustand geben.
3. **Intuitive Semantik:** Die Regeln der Sprache sollen den Entwickler aktiv zu sicheren und korrekten Mustern anleiten ("Pit of Success"). "Magisches" oder unvorhersehbares Verhalten des Compilers soll vermieden werden.
4. **Pragmatismus:** Ein bekannter, imperativer Programmierstil soll für lokale Logik weiterhin möglich sein, um die Lernkurve flach zu halten.
## Ergebnis: Das finale Sprachdesign
Wir haben uns auf ein pragmatisches Hybrid-Modell geeinigt, das funktionale Sicherheit mit imperativem Komfort kombiniert. Es basiert auf drei Säulen:
### 1. Sicherer globaler Zustand: `def` erzeugt Atome
Der `def`-Befehl wird modifiziert. Er dient ausschließlich zur Definition von Zustand, der potenziell über Funktionsgrenzen hinweg geteilt wird. Um dies von Natur aus sicher zu machen, erzeugt `def` nicht mehr einen einfachen, veränderlichen "Slot", sondern immer einen **atomaren Container**, der den Wert umschließt. Jede Zustandsänderung muss über explizite, threadsichere atomare Operationen (wie `swap!` oder `reset!`) erfolgen. Dies eliminiert alle Race Conditions bei globalem Zustand.
### 2. Veränderlicher lokaler Zustand: `let` mit `assign`
Ein neues `let`-Konstrukt wird eingeführt, das sich an Clojure orientiert, um einen neuen lexikalischen Gültigkeitsbereich zu schaffen. Im Gegensatz zu Clojure sind die in `let` definierten Bindungen jedoch **standardmäßig veränderlich** und können über `assign` modifiziert werden. Dies erlaubt Entwicklern, für rein lokale Algorithmen (z.B. Schleifen mit Zählern) einen vertrauten, imperativen Stil zu verwenden, wo Threadsicherheit keine Rolle spielt.
### 3. Die Sicherheitsbrücke: Statische Analyse im Binder
Dies ist die zentrale Innovation, die beide Welten sicher miteinander verbindet. Der Compiler (speziell der Binder) führt eine **Escape Analysis** für Closures durch, um die missbräuchliche Freigabe von unsicherem, lokalem Zustand zu verhindern. Dabei gilt eine einzige, einfache Regel:
> **Eine Closure, die eine veränderliche let-Variable fängt, darf nicht aus dem Gültigkeitsbereich der Funktion zurückgegeben (oder global gespeichert) werden, in der diese Variable definiert wurde.**
* Wenn eine solche "zustandsbehaftete" Closure nur lokal verwendet wird (z.B. in einem `Map`), ist der Code gültig.
* Wenn versucht wird, eine solche Closure zurückzugeben oder einem globalen `def` zuzuweisen, erzeugt der Binder einen **Compile-Fehler**.
Dieser Fehler ist ein Feature, kein Mangel. Er zwingt den Entwickler, eine bewusste Entscheidung zu treffen: Entweder er strukturiert seinen Code um, oder er verwendet für den Zustand, der geteilt werden muss, explizit ein sicheres Primitiv (indem er die lokale Variable selbst zu einem `(atom ...)` macht).
Dieses Design vermeidet die Komplexität eines Borrow Checkers und die Unvorhersehbarkeit von "Compiler-Magie", indem es eine klare, leicht verständliche Regel zur Compile-Zeit durchsetzt.
---
## Nächste Schritte (Todo)
1. **Binder anpassen:** Implementierung der Escape Analysis für Closures. Der Binder muss `let`-Variablen, ihre Veränderung durch `assign` und das Fangen durch Closures nachverfolgen.
2. **Parser erweitern:** Hinzufügen der Syntax für das neue `let`-Statement.
3. **`def`-Implementierung ändern:** Die Laufzeitlogik für `def` muss so angepasst werden, dass sie atomare Container anstelle von rohen Werten verwaltet.
4. **RTL erweitern:** Die Laufzeit-Bibliothek muss um Funktionen zur Interaktion mit Atomen erweitert werden (z.B. `atom`, `deref`, `swap!`, `reset!`).
5. **Compiler-Fehlermeldung implementieren:** Eine klare und hilfreiche Fehlermeldung für den Fall entwerfen, dass die "Sicherheitsbrücke"-Regel verletzt wird.
+58
View File
@@ -0,0 +1,58 @@
# Projekt-Protokoll: Finalisierung des Sprachdesigns für Zustand
* **Datum:** 09. Oktober 2025
* **Zeit:** 12:26 CEST
## Motivation
Aufbauend auf dem initialen Entwurf eines sicheren Zustands-Modells geht es in diesem Schritt um die Konkretisierung und Verfeinerung des Sprachdesigns. Die Diskussion hat gezeigt, dass die Wahl der Schlüsselwörter (`def`, `let`) und die exakte Definition der Scope-Regeln entscheidend für die Intuivität und Sicherheit der Sprache sind. Ziel dieses Dokuments ist es, die finalen Entscheidungen festzuhalten.
## Ziel
1. **Etablierung unmissverständlicher Schlüsselwörter:** Die finalen Namen für die drei Zustands-Konstrukte (`atom`, `var`, `let`) sollen deren Semantik klar und deutlich widerspiegeln.
2. **Festlegung der Scope-Regeln:** Die Rolle des `(do)`-Blocks als primärer Scope-erzeugender Mechanismus für lokalen Zustand wird formalisiert.
3. **Definition der Unveränderlichkeits-Garantie:** Die Regeln für die Compile-Zeit-Überprüfung der Unveränderlichkeit von `let`-Bindungen werden festgelegt.
## Ergebnis: Das finale Drei-Säulen-Modell für Zustand
Wir haben uns auf ein klares Modell mit drei spezialisierten Konstrukten für die Zustandsverwaltung geeinigt, das maximale Sicherheit bei gleichzeitig hoher Flexibilität und Verständlichkeit bietet.
### 1. Die drei Konstrukte
Die folgende Tabelle fasst die Eigenschaften der drei finalen Konstrukte zusammen:
| Konstrukt | Geltungsbereich | Veränderlichkeit | Threadsicherheit |
| :--- | :--- | :--- | :--- |
| **`atom`** | Global / Geteilt | Veränderlich (atomar) | **Ja (Design-Ziel)** |
| **`var`** | Lokal (`do`-Block) | Veränderlich | Nein (by Design) |
| **`let`** | Lokal (`do`-Block) | **Unveränderlich** | Ja (da unveränderlich) |
### 2. Syntaktische Form
Alle drei Konstrukte folgen einer einheitlichen, einfachen Syntax für die Deklaration und optionale Initialisierung:
* `(atom symbol initial-value)`
* `(var symbol initial-value)`
* `(let symbol initial-value)`
### 3. Implikation für `(do)`
Der `(do)`-Block (`IBlockExpressionNode`) wird zum **zentralen und einzigen Mechanismus für die Erzeugung lexikalischer Geltungsbereiche** für lokalen Zustand (`var` und `let`). Jedes Vorkommen von `(do ...)` öffnet einen neuen Scope, der nach Abarbeitung des Blocks wieder zerstört wird. Dies ermöglicht ein intuitives, imperativ anmutendes Programmieren mit klar definierten Lebenszeiten für lokale Variablen.
---
## Nächste Schritte (Todo)
1. **Parser anpassen:**
* Die Schlüsselwörter `def` und `let` werden durch `atom`, `var` und `let` ersetzt.
* Alle drei Konstrukte werden initial zum selben `IVariableDeclarationNode` geparst. Eine Unterscheidung, um welches Konstrukt es sich handelt, wird im Binder getroffen.
2. **Binder anpassen (Kernaufgabe):**
* **`do`-Scoping:** `VisitBlockExpression` wird so erweitert, dass es bei jedem Aufruf einen neuen Scope (`EnterScope`/`ExitScope`) verwaltet.
* **Symbol-Verfolgung:** Der Binder muss für jede deklarierte Variable speichern, ob sie via `atom`, `var` oder `let` erzeugt wurde.
* **Unveränderlichkeits-Prüfung:** Beim Besuch eines `(assign ...)`-Knotens (`VisitAssignment`) muss der Binder prüfen, ob die Zielvariable als `let` deklariert wurde. Wenn ja, wird ein **Compile-Fehler** ausgelöst.
* **Escape Analysis verfeinern:** Die Sicherheitsprüfung wird so angepasst, dass sie nur noch bei Closures greift, die eine **`var`-Variable** fangen. Closures, die `let`-Variablen fangen, sind immer sicher.
3. **RTL & Evaluator anpassen:**
* Die Laufzeitlogik für `def` wird zur Implementierung für `(atom ...)` und erzeugt einen atomaren Container.
* Die RTL wird um die Funktionen `deref`, `reset!` und `swap!` erweitert.
+134
View File
@@ -0,0 +1,134 @@
-----
### Projektplan: Atome und das "Builder/Snapshot"-Pattern
* **Datum:** 09. Oktober 2025
* **Uhrzeit:** 16:07
#### Motivation
Die bisherige Diskussion hat zwei grundlegende Anforderungen an die Zustandsverwaltung herausgearbeitet:
1. **Allgemeine Sicherheit:** Für generische Datenstrukturen (Maps, Vektoren) bieten persistente Datenstrukturen durch ihre Immutabilität und lock-freie Lesbarkeit die höchste Sicherheit und Flexibilität bei konkurrierendem Zugriff.
2. **Spezialisierte Performance:** Für bestimmte Anwendungsfälle (z.B. große, append-only Time-Series) ist die Performance einer In-Place-Mutation überlegen, aber das Teilen des Zustands zwischen Threads erfordert eine sichere Synchronisation.
Die Motivation ist daher, ein **standardisiertes, generisches Muster** zu definieren, das die rohe Performance einer mutierbaren "Arbeitskopie" mit der Sicherheit von unveränderlichen, lesbaren "Snapshots" für die konkurrierende Analyse kombiniert.
#### Ziel
1. Etablierung des **"Builder/Snapshot"-Patterns** als idiomatisches Kernkonzept der Sprache für den Umgang mit hoch-performantem, geteiltem, mutablem Zustand.
2. Definition von **generischen Basis-Interfaces** (`IBuilder`, `IImmutable`), die dieses Pattern im Typensystem verankern.
3. Nahtlose Integration dieses Patterns mit den bestehenden `atom`- (für geteilten Zustand) und `var`- (für lokalen Zustand) Konstrukten.
#### Ergebnis
Das Ergebnis ist ein klares, zweigleisiges Modell für die Zustandsverwaltung, das auf dem generischen "Builder/Snapshot"-Pattern aufbaut.
**1. Das generische Pattern: `IBuilder` / `IImmutable`**
Wir definieren zwei konzeptionelle Basis-Interfaces, die den Vertrag des Patterns abbilden:
* `IImmutable`: Ein **Marker-Interface**, das einen sicheren, unveränderlichen "Produkt"- oder Snapshot-Typ kennzeichnet.
* `IBuilder`: Ein Interface, das einen "Builder" kennzeichnet. Es definiert die Fähigkeit, ein `IImmutable`-Produkt zu erzeugen.
**2. Die Spezialisierung (Beispiel: `Series`-Interfaces)**
Konkrete Datenstrukturen wie die `Series` spezialisieren diese generischen Rollen.
```delphi
// --- Generische Basis-Interfaces ---
// Kennzeichnet ein sicheres, unveränderliches Lese-Objekt (Snapshot).
IImmutable = interface(IInterface)
end;
// Kennzeichnet ein mutierbares Builder-Objekt, das Snapshots von sich erzeugen kann.
IBuilder = interface(IInterface)
function CreateSnapshot: IImmutable;
end;
// --- Spezialisierte Interfaces für die 'Series' ---
// ISeries ist die spezialisierte, lesbare Form von IImmutable.
ISeries = interface(IImmutable)
function GetCount: Int64;
function GetItems(Idx: Integer): TScalar;
property Count: Int64 read GetCount;
property Items[Idx: Integer]: TScalar read GetItems; default;
end;
// IWriteableSeries ist der spezialisierte Builder.
// Wichtig: Er implementiert den IBuilder-Vertrag, indem er eine CreateSnapshot-Methode
// anbietet, die den spezialisierten Snapshot-Typ ISeries zurückgibt.
IWriteableSeries = interface(IBuilder)
function GetCount: Int64;
property Count: Int64 read GetCount;
procedure Add(const Item: TScalar.TValue; Lookback: Int64 = -1);
function CreateSnapshot: ISeries; // Covarianter Return-Type zum Basis-Interface
end;
```
**3. Die Anwendungsbeispiele in der Sprache**
Das Pattern wird auf zwei Arten verwendet, je nach Kontext (`atom` oder `var`). Die RTL-Funktionen arbeiten mit den generischen `IBuilder`- und `IImmutable`-Konzepten.
**Beispiel 1: Geteilter Zustand (via `atom`)**
Der `atom` schützt den `IBuilder` vor konkurrierenden Schreibzugriffen.
```lisp
(*
* Szenario: Ein Ticker-Modul (Thread A) empfängt Live-Daten,
* während eine UI (Thread B) die Daten zur Analyse anfordert.
*)
; Ein globaler 'atom' wird erstellt. Er hält die einzige Instanz des Builders.
(atom live-ticker (create-series-builder))
; -- Thread A (Produzent) --
; Modifiziert den Builder sicher über eine generische, synchronisierte RTL-Funktion,
; die auf jedem IBuilder in einem Atom funktioniert.
(builder-update! live-ticker series-add new-tick-value)
; -- Thread B (Konsument) --
; Fordert einen sicheren, unveränderlichen Snapshot an.
; Die generische 'snapshot'-Funktion ruft intern 'CreateSnapshot' auf.
(let chart-data (snapshot live-ticker))
; 'chart-data' ist nun eine sichere ISeries (IImmutable), die ohne Locks gelesen werden kann.
(calculate-indicators chart-data)
```
**Beispiel 2: Lokaler Zustand (via `var`)**
Dies ist der "Maschinenraum"-Modus für maximale Single-Thread-Performance.
```lisp
(*
* Szenario: Innerhalb einer Funktion wird eine komplexe Datenreihe
* temporär aufgebaut, um ein einmaliges Ergebnis zu berechnen.
*)
(do
; Erzeuge einen lokalen, unsynchronisierten Builder.
(var local-builder (create-series-builder))
; Befülle den Builder direkt und ohne Lock-Overhead über typspezifische,
; unsynchronisierte RTL-Funktionen.
(repeat 1000000
(series-add! local-builder (get-next-value)))
; Erzeuge einen finalen Snapshot für die Weiterverarbeitung.
(let final-data (series-create-snapshot local-builder))
; Gib das Ergebnis der Analyse zurück.
(run-analysis-on final-data))
```
#### Todo
* [ ] Generische `IBuilder`- und `IImmutable`-Interfaces in Delphi definieren.
* [ ] Finale `ISeries`- und `IWriteableSeries`-Interfaces (als Spezialisierungen) implementieren.
* [ ] Eine konkrete `TSeries`-Klasse implementieren, die `IWriteableSeries` implementiert.
* [ ] Die generischen, synchronisierten RTL-Funktionen `(builder-update! ...)` und `(snapshot ...)` für `atom`s erstellen, die auf `IBuilder` operieren.
* [ ] Die spezifischen, unsynchronisierten RTL-Funktionen `(series-add! ...)` und `(series-create-snapshot ...)` für die `TSeries`-Implementierung erstellen.
* [ ] Unit-Tests für beide Anwendungsfälle schreiben.
+49
View File
@@ -0,0 +1,49 @@
### Von "Guaranteed Safety" zu "Guided Safety"
Die Lösung ist nicht, die Sicherheitsanalyse abzuschaffen, sondern ihre Konsequenz zu ändern. Der Compiler sollte nicht als sturer Torwächter agieren, sondern als intelligenter Assistent, der den Nutzer aufklärt und ihm eine bewusste Entscheidung ermöglicht.
**Der neue Ansatz:**
1. Der Compiler **erkennt** das potenziell unsichere Muster (eine Closure fängt eine `var`-Variable und verlässt ihren Scope) genau wie bisher.
2. Statt eines harten **Fehlers** erzeugt er eine detaillierte **Warnung**.
3. Diese Warnung erklärt das Risiko präzise und für Laien verständlich: "Diese Funktion ist nicht threadsicher. Für isolierte Berechnungen wie Backtests ist das in Ordnung. Wenn Sie sie aber zwischen Threads teilen, kann es zu Fehlern kommen."
4. Die Warnung schlägt die Lösung vor: "Wenn Sie dies beabsichtigen, kennzeichnen Sie die Funktion explizit als nicht-threadsicher, indem Sie `fn` durch `fn!` ersetzen."
5. Der Nutzer ändert `fn` zu `fn!`. Die Warnung verschwindet. Der Nutzer hat gelernt, eine bewusste Entscheidung getroffen und kann weiterarbeiten, ohne seinen Code komplett umstrukturieren zu müssen.
Dies erreicht das Beste aus beiden Welten: Sicherheit wird nicht aufgegeben, sondern der Nutzer wird über die Risiken aufgeklärt und muss die Verantwortung explizit übernehmen.
---
Ich fasse das Ergebnis in einem neuen Projektplan-Eintrag zusammen.
### Projektplan: "Guided Safety" und der `fn!`-Modifikator
* **Datum:** 09. Oktober 2025
* **Uhrzeit:** 17:47
#### Motivation
Die strikte "safe-by-design"-Garantie, die das "Escaping" von `var`-Variablen durch einen Compiler-Fehler verhindert, erweist sich für die Zielgruppe der programmiertechnisch unerfahrenen Domänen-Experten als zu restriktiv. Sie zwingt zur Umstrukturierung von Code, der im primären Anwendungsfall (isolierte Single-Thread-Berechnungen wie Backtests) sowohl korrekt als auch performant ist. Dies führt zu Frustration und mindert die Akzeptanz der Sprache.
#### Ziel
Das Sicherheitsmodell der Sprache wird von einer rigiden **"Guaranteed Safety"** zu einer benutzerfreundlichen **"Guided Safety"** weiterentwickelt. Der Compiler soll den Nutzer auf potenzielle Gefahren hinweisen, ihn aufklären und ihm ermöglichen, eine explizite, informierte Entscheidung zu treffen, anstatt ihn mit einem harten Fehler zu blockieren.
#### Ergebnis
1. **Compiler-Warnung statt Fehler:** Der Binder wird so angepasst, dass das Fangen einer `var`-Variable durch eine "escapende" Closure nicht mehr zu einem Compiler-Fehler, sondern zu einer detaillierten **Warnung** führt.
2. **Einführung von `fn!`:** Ein neuer Funktions-Deklarations-Syntax `(fn! ...)` wird eingeführt. Dieser Modifikator signalisiert dem Compiler: "Der Programmierer hat die Warnung zur Kenntnis genommen und deklariert diese Closure absichtlich als potenziell nicht-threadsicher." Das Vorhandensein von `fn!` unterdrückt die entsprechende Compiler-Warnung.
3. **Pädagogische Fehlermeldung:** Die Warnmeldung wird so formuliert, dass sie den Sachverhalt erklärt und direkt die Lösung (`fn!` verwenden) vorschlägt. Beispiel:
> **Warnung:** Die Funktion erfasst die veränderliche Variable 'sum', die außerhalb dieses Bereichs definiert wurde. Dies macht die Funktion nicht threadsicher. Für isolierte Berechnungen (z.B. in einem Backtest) ist dies unproblematisch und performant. Um zu bestätigen, dass dies beabsichtigt ist, ändern Sie `(fn ...)` zu `(fn! ...)`."
4. **Beibehaltung der sicheren Muster:** Die bestehenden, sicheren Muster (`atom` für geteilten Zustand, `var` mit zustandslosen Funktionen) bleiben die empfohlenen Standardlösungen. `fn!` dient als explizite, bewusste Abweichung für den gut verstandenen Performance-Fall.
#### Todo
* [ ] Die Logik im `Binder` anpassen, um eine Warnung anstelle eines Fehlers für "escapende `var`-Variablen" zu generieren.
* [ ] Den `Parser` um die Erkennung des `fn!`-Schlüsselworts erweitern.
* [ ] Die Binder-Logik erweitern, sodass `fn!` die Warnung unterdrückt.
* [ ] Die exakte Formulierung der neuen Warnmeldung implementieren.
* [ ] Die Dokumentation aktualisieren, um das "Guided Safety"-Konzept und die korrekte Verwendung von `fn`, `fn!` und `atom` zu erklären.
+110
View File
@@ -0,0 +1,110 @@
Die bisherige Diskussion hat sich um die Frage gedreht: "Was ist theoretisch sicher?". Du hast die Perspektive entscheidend auf die Frage gelenkt: "**Was ist praktisch nützlich und führt zu einer hohen Akzeptanz bei der Zielgruppe?**"
Ein System, das einen Domänen-Experten zwingt, ein funktionierendes, intuitives Muster in ein kompliziertes, für ihn abstraktes Muster umzubauen, nur um einen Compiler zufriedenzustellen, hat seinen Zweck verfehlt. Der Nutzer wird frustriert aufgeben.
Deine Schlussfolgerung ist daher richtig: Die "safe by design"-Garantie in ihrer strengsten Form (als harter Compiler-Fehler) ist für diesen Anwendungsfall ein Hindernis. Wir müssen das Design weiterentwickeln.
---
### Von "Guaranteed Safety" zu "Guided Safety"
Die Lösung ist nicht, die Sicherheitsanalyse abzuschaffen, sondern ihre Konsequenz zu ändern. Der Compiler sollte nicht als sturer Torwächter agieren, sondern als intelligenter Assistent, der den Nutzer aufklärt und ihm eine bewusste Entscheidung ermöglicht.
**Der neue Ansatz:**
1. Der Compiler **erkennt** das potenziell unsichere Muster (eine Closure fängt eine `var`-Variable und verlässt ihren Scope) genau wie bisher.
2. Statt eines harten **Fehlers** erzeugt er eine detaillierte **Warnung**.
3. Diese Warnung erklärt das Risiko präzise und für Laien verständlich: "Diese Funktion ist nicht threadsicher. Für isolierte Berechnungen wie Backtests ist das in Ordnung. Wenn Sie sie aber zwischen Threads teilen, kann es zu Fehlern kommen."
4. Die Warnung schlägt die Lösung vor: "Wenn Sie dies beabsichtigen, kennzeichnen Sie die Funktion explizit als nicht-threadsicher, indem Sie `fn` durch `fn!` ersetzen."
5. Der Nutzer ändert `fn` zu `fn!`. Die Warnung verschwindet. Der Nutzer hat gelernt, eine bewusste Entscheidung getroffen und kann weiterarbeiten, ohne seinen Code komplett umstrukturieren zu müssen.
Dies erreicht das Beste aus beiden Welten: Sicherheit wird nicht aufgegeben, sondern der Nutzer wird über die Risiken aufgeklärt und muss die Verantwortung explizit übernehmen.
---
Ich fasse das Ergebnis in einem neuen Projektplan-Eintrag zusammen.
### Projektplan: "Guided Safety" und der `fn!`-Modifikator
* **Datum:** 09. Oktober 2025
* **Uhrzeit:** 17:47
#### Motivation
Die strikte "safe-by-design"-Garantie, die das "Escaping" von `var`-Variablen durch einen Compiler-Fehler verhindert, erweist sich für die Zielgruppe der programmiertechnisch unerfahrenen Domänen-Experten als zu restriktiv. Sie zwingt zur Umstrukturierung von Code, der im primären Anwendungsfall (isolierte Single-Thread-Berechnungen wie Backtests) sowohl korrekt als auch performant ist. Dies führt zu Frustration und mindert die Akzeptanz der Sprache.
#### Ziel
Das Sicherheitsmodell der Sprache wird von einer rigiden **"Guaranteed Safety"** zu einer benutzerfreundlichen **"Guided Safety"** weiterentwickelt. Der Compiler soll den Nutzer auf potenzielle Gefahren hinweisen, ihn aufklären und ihm ermöglichen, eine explizite, informierte Entscheidung zu treffen, anstatt ihn mit einem harten Fehler zu blockieren.
#### Ergebnis
1. **Compiler-Warnung statt Fehler:** Der Binder wird so angepasst, dass das Fangen einer `var`-Variable durch eine "escapende" Closure nicht mehr zu einem Compiler-Fehler, sondern zu einer detaillierten **Warnung** führt.
2. **Einführung von `fn!`:** Ein neuer Funktions-Deklarations-Syntax `(fn! ...)` wird eingeführt. Dieser Modifikator signalisiert dem Compiler: "Der Programmierer hat die Warnung zur Kenntnis genommen und deklariert diese Closure absichtlich als potenziell nicht-threadsicher." Das Vorhandensein von `fn!` unterdrückt die entsprechende Compiler-Warnung.
3. **Pädagogische Fehlermeldung:** Die Warnmeldung wird so formuliert, dass sie den Sachverhalt erklärt und direkt die Lösung (`fn!` verwenden) vorschlägt. Beispiel:
> **Warnung:** Die Funktion erfasst die veränderliche Variable 'sum', die außerhalb dieses Bereichs definiert wurde. Dies macht die Funktion nicht threadsicher. Für isolierte Berechnungen (z.B. in einem Backtest) ist dies unproblematisch und performant. Um zu bestätigen, dass dies beabsichtigt ist, ändern Sie `(fn ...)` zu `(fn! ...)`."
4. **Beibehaltung der sicheren Muster:** Die bestehenden, sicheren Muster (`atom` für geteilten Zustand, `var` mit zustandslosen Funktionen) bleiben die empfohlenen Standardlösungen. `fn!` dient als explizite, bewusste Abweichung für den gut verstandenen Performance-Fall.
-------------------
> Jetzt weiß der Compiler, ob eine Closure unsafe ist. Kann er sonst noch was mit dieser Information anfangen?
Ja, absolut. Das ist eine hervorragende Frage, die den Kern von modernem Compiler-Design trifft. Die Information, ob eine Closure potenziell unsicher ist, ist weit mehr als nur ein Schalter zum Unterdrücken einer Warnung es ist ein wertvolles **Metadatum**, eine semantische Information, die das gesamte Ökosystem nutzen kann.
Stell dir das `!` bei `fn!` wie einen **"Gefahrgut"-Aufkleber** auf einem Paket vor. Es sagt nicht nur dem Absender "Vorsicht", sondern informiert die gesamte Lieferkette (Compiler, Laufzeitumgebung, andere Entwickler) darüber, wie dieses Paket zu behandeln ist.
Hier sind konkrete, extrem nützliche Dinge, die der Compiler und die Laufzeitumgebung mit dieser Information tun können:
-----
### 1\. Laufzeit-Überprüfung im Debug-Modus 💥
Das ist der größte Gewinn. Wenn der Compiler weiß, dass eine `fn!`-Closure erzeugt wird, kann er speziellen Code für den Debug-Build generieren:
1. **Beim Erstellen der Closure:** Die Laufzeitumgebung speichert die `ThreadID` des erstellenden Threads direkt in der Closure-Struktur.
2. **Bei jedem Aufruf der Closure:** Die Laufzeitumgebung prüft: `if CurrentThreadID != CapturedThreadID then`.
3. **Wenn die IDs nicht übereinstimmen**, wird sofort eine **harte Exception** ausgelöst mit einer extrem klaren Fehlermeldung:
> "Laufzeitfehler: Die nicht-threadsichere Funktion, die in Thread A (ID: 1234) erstellt wurde, wurde fälschlicherweise von Thread B (ID: 5678) aufgerufen. Dies hätte zu einer Race Condition geführt."
**Der Vorteil ist gigantisch:** Ein unvorhersehbarer, schwer zu findender Heisenbug (eine Race Condition, die nur manchmal auftritt) wird in einen **100% reproduzierbaren, sofortigen Absturz** mit perfekter Fehlerbeschreibung umgewandelt. Allein dieses Feature macht `fn!` von einer "gefährlichen" zu einer extrem nützlichen, fehlertoleranten Abstraktion.
-----
### 2\. Erweiterte Statische Analyse (Linting) 🧐
Der Compiler (oder ein separates Linter-Tool) kann diese Information für intelligentere Code-Prüfungen nutzen. Er kann die "Gefahrgut-Pakete" verfolgen:
Angenommen, es gibt eine Funktion `(parallel-map ...)` in der Standardbibliothek, die dafür bekannt ist, ihre Arbeits-Funktion auf mehreren Threads auszuführen. Die Signatur dieser Funktion kann intern vermerken, dass sie nur threadsichere Closures akzeptiert.
Wenn ein Entwickler nun versucht, Folgendes zu schreiben:
```lisp
(let my-unsafe-sma (create-sma-unsafe 20)) ; Gibt eine fn!-Closure zurück
(parallel-map my-unsafe-sma my-big-data-list)
```
Kann der Compiler an dieser Stelle eine noch spezifischere und ernstere **Warnung oder sogar einen Fehler** ausgeben:
> "Typenkonflikt: `parallel-map` erwartet eine threadsichere Funktion, aber `my-unsafe-sma` wurde mit `fn!` als nicht-threadsicher deklariert. Dies führt garantiert zu Datenkorruption."
-----
### 3\. Automatische API-Dokumentation 📖
Ein Dokumentations-Generator kann die `fn!`-Metadaten auslesen und automatisch Warnhinweise in die generierte HTML-Doku einfügen. Wenn jemand die Dokumentation für deine `CreateSMA`-Funktion liest, könnte dort stehen:
**`CreateSMA(len: Integer): Function`**
> Erzeugt eine Funktion zur Berechnung eines gleitenden Durchschnitts.
>
> ⚠️ **Thread-Sicherheit:** Diese Funktion gibt eine optimierte, nicht-threadsichere Funktion (`fn!`) zurück. Das Ergebnis darf unter keinen Umständen zwischen verschiedenen Threads geteilt werden.
Dadurch wird der API-Vertrag für jeden sofort ersichtlich, ohne dass man den Quellcode lesen muss.
-----
### Fazit
Die explizite Kennzeichnung mit `fn!` ist also kein Rückschritt in der Sicherheit. Im Gegenteil, es ist eine **semantische Anreicherung**. Sie verwandelt das Konzept von einer reinen Warnungs-Unterdrückung in einen expliziten **Vertrag**, den die gesamte Werkzeugkette Compiler, Debugger, Linter, Doku-Generator versteht und nutzen kann, um den Entwickler noch besser und intelligenter zu leiten.
-105
View File
@@ -1,105 +0,0 @@
\*\*\*
\# Projekt-Protokoll: Weiterentwicklung des Sprachdesigns
\* \*\*Datum:\*\* 08. Oktober 2025
\* \*\*Zeit:\*\* 13:22 CEST
\## Motivation
Die bestehende Skriptsprache bietet grundlegende funktionale und imperative Konstrukte, aber die Verwaltung von veränderlichem Zustand (`def` in Verbindung mit `assign`) ist unsicher, insbesondere im Hinblick auf Nebenläufigkeit. Das primäre Motiv für eine Weiterentwicklung ist die Schaffung einer Sprache, die \*\*aus sich selbst heraus sicher ("safe by design")\*\* ist, ohne dabei die Komplexität von Low-Level-Konzepten wie manueller Speicherverwaltung oder einem vollständigen Borrow-Checker (wie in Rust) einzuführen. Die Sprache soll auch für Programmier-Anfänger \*\*intuitiv und leicht verständlich\*\* bleiben.
\## Ziel
Das Ziel ist es, ein Sprachdesign zu entwickeln, das eine klare und sichere Handhabung von Zustand erzwingt. Die Kernziele des neuen Designs sind:
1\. \*\*Sicherheit durch Design:\*\* Race Conditions durch geteilten, veränderlichen Zustand sollen zur Compile-Zeit erkannt und verhindert werden.
2\. \*\*Klare Trennung von Zustandsarten:\*\* Es soll eine unmissverständliche Unterscheidung zwischen temporärem, lokalem Zustand und persistentem, geteiltem Zustand geben.
3\. \*\*Intuitive Semantik:\*\* Die Regeln der Sprache sollen den Entwickler aktiv zu sicheren und korrekten Mustern anleiten ("Pit of Success"). "Magisches" oder unvorhersehbares Verhalten des Compilers soll vermieden werden.
4\. \*\*Pragmatismus:\*\* Ein bekannter, imperativer Programmierstil soll für lokale Logik weiterhin möglich sein, um die Lernkurve flach zu halten.
\## Ergebnis: Das finale Sprachdesign
Wir haben uns auf ein pragmatisches Hybrid-Modell geeinigt, das funktionale Sicherheit mit imperativem Komfort kombiniert. Es basiert auf drei Säulen:
\### 1. Sicherer globaler Zustand: `def` erzeugt Atome
Der `def`-Befehl wird modifiziert. Er dient ausschließlich zur Definition von Zustand, der potenziell über Funktionsgrenzen hinweg geteilt wird. Um dies von Natur aus sicher zu machen, erzeugt `def` nicht mehr einen einfachen, veränderlichen "Slot", sondern immer einen \*\*atomaren Container\*\*, der den Wert umschließt. Jede Zustandsänderung muss über explizite, threadsichere atomare Operationen (wie `swap!` oder `reset!`) erfolgen. Dies eliminiert alle Race Conditions bei globalem Zustand.
\### 2. Veränderlicher lokaler Zustand: `let` mit `assign`
Ein neues `let`-Konstrukt wird eingeführt, das sich an Clojure orientiert, um einen neuen lexikalischen Gültigkeitsbereich zu schaffen. Im Gegensatz zu Clojure sind die in `let` definierten Bindungen jedoch \*\*standardmäßig veränderlich\*\* und können über `assign` modifiziert werden. Dies erlaubt Entwicklern, für rein lokale Algorithmen (z.B. Schleifen mit Zählern) einen vertrauten, imperativen Stil zu verwenden, wo Threadsicherheit keine Rolle spielt.
\### 3. Die Sicherheitsbrücke: Statische Analyse im Binder
Dies ist die zentrale Innovation, die beide Welten sicher miteinander verbindet. Der Compiler (speziell der Binder) führt eine \*\*Escape Analysis\*\* für Closures durch, um die missbräuchliche Freigabe von unsicherem, lokalem Zustand zu verhindern. Dabei gilt eine einzige, einfache Regel:
> \*\*Eine Closure, die eine veränderliche let-Variable fängt, darf nicht aus dem Gültigkeitsbereich der Funktion zurückgegeben (oder global gespeichert) werden, in der diese Variable definiert wurde.\*\*
\* Wenn eine solche "zustandsbehaftete" Closure nur lokal verwendet wird (z.B. in einem `Map`), ist der Code gültig.
\* Wenn versucht wird, eine solche Closure zurückzugeben oder einem globalen `def` zuzuweisen, erzeugt der Binder einen \*\*Compile-Fehler\*\*.
Dieser Fehler ist ein Feature, kein Mangel. Er zwingt den Entwickler, eine bewusste Entscheidung zu treffen: Entweder er strukturiert seinen Code um, oder er verwendet für den Zustand, der geteilt werden muss, explizit ein sicheres Primitiv (indem er die lokale Variable selbst zu einem `(atom ...)` macht).
Dieses Design vermeidet die Komplexität eines Borrow Checkers und die Unvorhersehbarkeit von "Compiler-Magie", indem es eine klare, leicht verständliche Regel zur Compile-Zeit durchsetzt.
---
\## Nächste Schritte (Todo)
1\. \*\*Binder anpassen:\*\* Implementierung der Escape Analysis für Closures. Der Binder muss `let`-Variablen, ihre Veränderung durch `assign` und das Fangen durch Closures nachverfolgen.
2\. \*\*Parser erweitern:\*\* Hinzufügen der Syntax für das neue `let`-Statement.
3\. \*\*`def`-Implementierung ändern:\*\* Die Laufzeitlogik für `def` muss so angepasst werden, dass sie atomare Container anstelle von rohen Werten verwaltet.
4\. \*\*RTL erweitern:\*\* Die Laufzeit-Bibliothek muss um Funktionen zur Interaktion mit Atomen erweitert werden (z.B. `atom`, `deref`, `swap!`, `reset!`).
5\. \*\*Compiler-Fehlermeldung implementieren:\*\* Eine klare und hilfreiche Fehlermeldung für den Fall entwerfen, dass die "Sicherheitsbrücke"-Regel verletzt wird.
+8 -3
View File
@@ -50,15 +50,20 @@ implementation
function TInterpreterScopeTests.Execute(const ANode: IAstNode): TDataValue; function TInterpreterScopeTests.Execute(const ANode: IAstNode): TDataValue;
var var
binder: IAstBinder;
boundNode: IAstNode; boundNode: IAstNode;
descriptor: IScopeDescriptor; descriptor: IScopeDescriptor;
runtimeScope: IExecutionScope; runtimeScope: IExecutionScope;
visitor: IEvaluatorVisitor; visitor: IEvaluatorVisitor;
begin begin
// Helper to encapsulate the Bind -> CreateScope -> Evaluate pattern. // Helper to encapsulate the Bind -> CreateScope -> Evaluate pattern.
binder := TAstBinder.Create(FGlobalScope); // The binder needs an evaluator factory to expand macros (even if there are none).
boundNode := binder.Execute(ANode, descriptor); boundNode :=
TAstBinder.Bind(
FGlobalScope,
ANode,
descriptor,
function(const Scope: IExecutionScope): IEvaluatorVisitor begin Result := TEvaluatorVisitor.Create(Scope); end
);
runtimeScope := descriptor.CreateScope(FGlobalScope); runtimeScope := descriptor.CreateScope(FGlobalScope);
visitor := TEvaluatorVisitor.Create(runtimeScope); visitor := TEvaluatorVisitor.Create(runtimeScope);
Result := visitor.Execute(boundNode); Result := visitor.Execute(boundNode);