feat: Document AST node structure across compiler stages

Add a new Markdown document detailing the AST node structure as it
evolves through the compiler's different stages. This includes a diagram
illustrating the data flow and relationships between the generic
`Node<K, T>` structure and its specialized forms (`UntypedNode`,
`BoundNode`, `TypedNode`, `AnalyzedNode`). The document explains the
`kind` and `ty` payloads for each stage and their significance.

Also removes a deleted file related to a previous optimization plan and
adds new files for future optimization plans.
This commit is contained in:
Michael Schimmel
2026-02-27 08:58:38 +01:00
parent e104e7f59b
commit 83324a1892
5 changed files with 249 additions and 55 deletions
@@ -0,0 +1,73 @@
# Optimierung des Speicherlayouts für Series und Records
## Motivation
Der Kern einer jeden DSL für die Finanzanalyse ist die Verarbeitung enormer Mengen an Zeitreihendaten (Ticks, Kerzen, abgeleitete Indikatoren). Diese Daten fließen in Form von **Series**, **Streams** und **Pipes** durch das System.
In der aktuellen Rust-Portierung basiert die Laufzeitumgebung (`VM`) vollständig auf dem `Value`-Enum:
```rust
pub enum Value {
Int(i64),
Float(f64),
Record(Arc<RecordLayout>, Rc<Vec<Value>>),
// ...
}
```
Auf einem 64-Bit-System hat dieses Enum durch Alignment und Tagging eine Größe von exakt **24 Bytes**. Jedes `Value::Int(42)` oder `Value::Float(3.14)` belegt somit das Dreifache an Speicher im Vergleich zu einem nativen 8-Byte Skalar.
Für einzelne lokale Variablen ist das vernachlässigbar. Für Zeitreihen führt dieses Layout jedoch zu massiven Problemen:
1. **Cache Misses:** Arrays von 24-Byte-Strukturen zerstören die Cache-Lokalität, die für rechenintensive Indikatoren essentiell ist.
2. **Heap Allokationen:** Ein `Value::Record` erzeugt für seine Felder einen `Rc<Vec<Value>>`. Ein einfaches `push(series, { price: 100.0, volume: 10 })` würde aktuell jedes Mal eine Heap-Allokation für den Record auslösen.
3. **Pointer Chasing:** Das ständige Dereferenzieren von Rc/Arc-Pointern in engen Loops verhindert Vektorisierung (SIMD) auf CPU-Ebene.
In der alten Delphi-Codebase wurde dies durch spezielle Code-Pfade für skalare Typen gelöst. In Rust können wir dank des `Specializers` und des statischen Typsystems einen noch effizienteren Weg gehen.
## Konzept: Data-Oriented Design & Struct of Arrays (SoA)
Anstatt Series als einfache Listen von `Value`-Objekten zu implementieren, werden Series basierend auf ihrem statischen Typ spezialisiert.
### Skalare Serien
Eine Serie von Float-Werten (`Series<Float>`) speichert intern keine `Value`s, sondern nutzt ein natives Array (z. B. einen Ringpuffer): `Vec<f64>`.
### Record Serien (SoA)
Besonders bei Records zeigt sich die Stärke von SoA. Angenommen, wir haben einen Record `Tick { price: Float, volume: Int }`.
Eine `RecordSeries` speichert **kein** Array von Structs (AoS), sondern spaltet die Felder in separate Arrays auf:
```rust
// Konzeptionelles Layout
struct TickSeries {
prices: Vec<f64>, // Perfekt sequenziell im Cache
volumes: Vec<i64>,
}
```
Wenn ein Skript z. B. den gleitenden Durchschnitt über `prices` berechnet, liest die CPU einen dichten Block von `f64` direkt in den L1-Cache, ohne jemals das `volume` laden zu müssen.
## Integration in die Compiler-Pipeline
Die Optimierung erfordert keine Änderungen an der Syntax der Skriptsprache. Der AST bleibt aus Sicht des Benutzers dynamisch, wird aber vom Compiler monomorphisiert ("Fast Paths").
### 1. Das `Object` Trait
Spezialisierte Serien (wie `FloatSeries` oder `TickSeries`) implementieren das bereits vorhandene `Object`-Trait. Dadurch können sie bei Bedarf in ein `Value::Object(Rc<dyn Object>)` verpackt und dynamisch durch das System gereicht werden.
### 2. Node Fusion im Specializer
Da der `Analyzer` den `StaticType` jeder Variable kennt, weiß der `Specializer` zur Compile-Zeit exakt, ob eine Variable eine generische Liste oder eine spezialisierte `FloatSeries` ist.
**Beispiel: Element auslesen**
`let p = my_series[0].price`
Anstatt einen generischen `GetElement` und `GetField` Node zu erzeugen, verschmilzt ("fusioniert") der Specializer dies zu einem spezialisierten VM-Node: `ExecNode::GetSeriesRecordFieldFloat`.
Zur Laufzeit führt dieser Node folgendes aus:
1. Holt die Serie via `downcast_ref` aus dem `Value::Object`.
2. Greift direkt auf `series.prices[0]` zu.
3. Boxed das Ergebnis *einmalig* in ein `Value::Float` (oder hält es auf dem Stack).
**Beispiel: Werte pushen (Zero-Allocation)**
`push(my_series, { price: 100.0, volume: 10 })`
Der Specializer erkennt, dass der Record nur für den Push erzeugt wird. Er generiert einen `PushToRecordSeries`-Node. Dieser Node erzeugt niemals ein `Value::Record` auf dem Heap. Stattdessen liest er die Argumente als primitive `f64`/`i64` und schreibt sie direkt in die parallelen Arrays der `TickSeries`.
## Nächste Schritte
1. **Prototyping:** Implementierung der primitiven Ringpuffer (`RingBuffer`) und der basischen Serien-Strukturen für Skalare und Records.
2. **Specializer-Erweiterung:** Erkennung von Typmustern im AST und Generierung der Fast-Path-Knoten (`BoundKind`-Erweiterungen).
3. **VM-Integration:** Anpassung der Evaluierungs-Loop, um die spezialisierten Knoten auszuführen und via `Any::downcast_ref` sicher und performant in die nativen Strukturen zu greifen.