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:
@@ -0,0 +1,118 @@
|
||||
# AST Node-Struktur über Compiler-Stages
|
||||
|
||||
Dieses Dokument beschreibt die Architektur der AST-Knoten (Abstract Syntax Tree) über die verschiedenen Phasen des Compilers hinweg. Die Rust-Implementierung nutzt eine generische `Node<K, T>` Struktur, die in jeder Phase strikt typisiert wird, um Fehlerklassen der ursprünglichen Delphi-Architektur zu vermeiden.
|
||||
|
||||
## Architektur & Datenfluss
|
||||
|
||||
Der Compiler verarbeitet den AST in mehreren Stufen. Statt die Knoten in-place zu mutieren, erzeugt jede Phase einen neuen, typsicheren und spezialisierten Baum.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction TB
|
||||
|
||||
class Node~K,T~ {
|
||||
+Identity identity
|
||||
+K kind
|
||||
+T ty
|
||||
}
|
||||
|
||||
class UntypedKind {
|
||||
<<enumeration>>
|
||||
Identifier(Symbol)
|
||||
Parameter(Symbol)
|
||||
Def(Target, Value)
|
||||
Assign(Target, Value)
|
||||
MacroDecl(...)
|
||||
Template(...)
|
||||
Splice(...)
|
||||
...
|
||||
}
|
||||
|
||||
class BoundKind~T~ {
|
||||
<<enumeration>>
|
||||
Get(Address, Symbol)
|
||||
Set(Address, Value)
|
||||
Define(Symbol, Address, Kind, Value, Captures)
|
||||
GetField(Record, Keyword)
|
||||
Destructure(Pattern, Value)
|
||||
Lambda(Params, Upvalues, Body)
|
||||
...
|
||||
}
|
||||
|
||||
class StaticType {
|
||||
<<struct>>
|
||||
// Type Information
|
||||
}
|
||||
|
||||
class NodeMetrics {
|
||||
<<struct>>
|
||||
+Rc~TypedNode~ original
|
||||
+Purity purity
|
||||
+bool is_recursive
|
||||
}
|
||||
|
||||
%% Typedefs und Spezialisierungen
|
||||
class UntypedNode {
|
||||
<<type alias>>
|
||||
Node~UntypedKind, ()~
|
||||
}
|
||||
|
||||
class BoundNode {
|
||||
<<type alias>>
|
||||
Node~BoundKind~()~, ()~
|
||||
}
|
||||
|
||||
class TypedNode {
|
||||
<<type alias>>
|
||||
Node~BoundKind~StaticType~, StaticType~
|
||||
}
|
||||
|
||||
class AnalyzedNode {
|
||||
<<type alias>>
|
||||
Node~BoundKind~NodeMetrics~, NodeMetrics~
|
||||
}
|
||||
|
||||
Node <|-- UntypedNode : binds
|
||||
UntypedNode *-- UntypedKind : uses
|
||||
|
||||
Node <|-- BoundNode : binds
|
||||
BoundNode *-- BoundKind : uses
|
||||
|
||||
Node <|-- TypedNode : binds
|
||||
TypedNode *-- StaticType : uses T
|
||||
TypedNode *-- BoundKind : uses K
|
||||
|
||||
Node <|-- AnalyzedNode : binds
|
||||
AnalyzedNode *-- NodeMetrics : uses T
|
||||
AnalyzedNode *-- BoundKind : uses K
|
||||
|
||||
%% Phasen-Übergänge
|
||||
UntypedNode ..> BoundNode : Parser & Binder
|
||||
(Namensauflösung)
|
||||
BoundNode ..> TypedNode : TypeChecker
|
||||
(Typ-Inferenz)
|
||||
TypedNode ..> AnalyzedNode : Analyzer
|
||||
(Purity, Rekursion)
|
||||
```
|
||||
|
||||
## Compiler Stages im Detail
|
||||
|
||||
1. **Parser (Untyped AST)**
|
||||
* **Typ:** `Node<UntypedKind, ()>` (Alias: Keiner, direkt genutzt)
|
||||
* **Beschreibung:** Der vom Parser erstellte Roh-Baum. Bezeichner (`Identifier`, `Def`, `Assign`) sind lediglich Namen (Symbols). Beinhaltet Strukturen für das Macro-System (`MacroDecl`, `Template`, `Splice`).
|
||||
* **Payload (`T`):** `()` (Keine Metadaten)
|
||||
|
||||
2. **Binder (Bound AST)**
|
||||
* **Typ:** `BoundNode<()>` bzw. `Node<BoundKind<()>, ()>`
|
||||
* **Beschreibung:** Namensauflösung und Scope-Analyse wurden durchgeführt. Namen wurden in physische Adressen (`Address`: `LocalSlot`, `UpvalueIdx`, `GlobalIdx`) überführt. Neue Knoten wie `Get`, `Set`, `Define` lösen die untypisierten Varianten ab. Closures kennen nun ihre `Upvalues` (gefangene Variablen). Macros wurden vollständig expandiert.
|
||||
* **Payload (`T`):** `()`
|
||||
|
||||
3. **Type Checker (Typed AST)**
|
||||
* **Typ:** `TypedNode` bzw. `Node<BoundKind<StaticType>, StaticType>`
|
||||
* **Beschreibung:** Statische Typprüfung. Die Kern-Struktur (`BoundKind`) bleibt erhalten, aber jeder Knoten trägt nun seinen verifizierten statischen Typ (`StaticType`) in der Eigenschaft `ty`.
|
||||
* **Payload (`T`):** `StaticType`
|
||||
|
||||
4. **Analyzer / Optimizer (Analyzed AST)**
|
||||
* **Typ:** `AnalyzedNode` bzw. `Node<BoundKind<NodeMetrics>, NodeMetrics>`
|
||||
* **Beschreibung:** Vorbereitung für die Optimierung. Sammelt wichtige Metadaten pro Knoten für den Optimizer.
|
||||
* **Payload (`T`):** `NodeMetrics` (enthält Metadaten wie `Purity` (Ist der Knoten seiteneffektfrei?), `is_recursive` sowie eine Referenz auf den originalen `TypedNode`)
|
||||
@@ -1,55 +0,0 @@
|
||||
# Plan: Record Layout & Optimized Field Access
|
||||
|
||||
This document outlines the implementation plan for porting Delphi's `TKeywordMappingRegistry` optimizations to the Rust-based Myc AST compiler and integrating first-class field accessors.
|
||||
|
||||
## 1. Core Data Structures (`src/ast/types.rs`)
|
||||
|
||||
### RecordLayout (The Schema)
|
||||
- **Concept:** Replaces `IKeywordMapping<IStaticType>`.
|
||||
- **Interning:** Managed via a global `OnceLock<Mutex<HashMap<..., Arc<RecordLayout>>>>`.
|
||||
- **FMap Optimization:**
|
||||
- Stores `fields: Vec<(Keyword, StaticType)>`.
|
||||
- Maintains a lookup array `Vec<i32>` mapping `Keyword.idx - first_key` to the index in the fields array.
|
||||
- Provides `index_of(Keyword) -> Option<usize>` with **O(1)** complexity.
|
||||
- **Thread Safety:** Uses `Arc` for global sharing across script threads.
|
||||
|
||||
### Value & StaticType Updates
|
||||
- `Value::Record` points to an `Arc<RecordLayout>` and a flat `Rc<Vec<Value>>`.
|
||||
- `Value::FieldAccessor(Keyword)` added as a first-class callable value (e.g., `.name`).
|
||||
- `StaticType::Record` now holds `Arc<RecordLayout>`.
|
||||
|
||||
## 2. Compiler Pipeline
|
||||
|
||||
### Parser (`src/ast/parser.rs`)
|
||||
- Identifiers starting with `.` (e.g., `.age`) are parsed as `UntypedKind::FieldAccessor(Keyword)`.
|
||||
- This ensures interning happens immediately; strings are never used for field access in subsequent phases.
|
||||
|
||||
### Binder (`src/ast/compiler/binder.rs`)
|
||||
- Maps `UntypedKind::FieldAccessor` to `BoundKind::FieldAccessor`.
|
||||
|
||||
### TypeChecker (`src/ast/compiler/type_checker.rs`)
|
||||
- **Analysis Only:** When encountering `Call { callee: FieldAccessor, args: [rec] }`:
|
||||
- Validates that `rec` is a Record.
|
||||
- Uses `RecordLayout::index_of` to find the field's type.
|
||||
- Sets the call's return type accordingly.
|
||||
- **Crucial:** Does NOT modify the AST structure. It remains a `Call`.
|
||||
|
||||
### Optimizer (`src/ast/compiler/optimizer/engine.rs`)
|
||||
- **Transformation:** Recognizes the `Call { callee: FieldAccessor, ... }` pattern.
|
||||
- Transforms it into a specialized `BoundKind::GetField { rec, field: Keyword }` node.
|
||||
|
||||
## 3. Execution (`src/ast/vm.rs`)
|
||||
|
||||
### Optimized Path (`GetField`)
|
||||
- The VM executes `GetField` by:
|
||||
1. Evaluating the record.
|
||||
2. Calling `layout.index_of(field)` (O(1)).
|
||||
3. Fetching the value from the flat array.
|
||||
|
||||
### Functional Path (`FieldAccessor`)
|
||||
- If `.field` is passed as a value (e.g., `(map .name users)`), the VM treats the `FieldAccessor` value as a standard function that takes one argument and performs the lookup.
|
||||
|
||||
## 4. Benefits
|
||||
- **Type Identity:** `Arc::ptr_eq` allows instant type comparison.
|
||||
- **Performance:** Field access in loops is O(1) without hash lookups.
|
||||
- **Safety:** The TypeChecker remains a pure analysis phase as per design requirements.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Record Optimization Plan
|
||||
|
||||
Dieses Dokument beschreibt geplante Optimierungen für Records im Myc-Compiler, um die Performance bei Datenstrukturen zu steigern.
|
||||
|
||||
## 1. Constant Folding für Record-Literale
|
||||
|
||||
**Status:** Aktuell werden Record-Literale in `engine.rs` nur rekursiv besucht, aber nicht gefaltet.
|
||||
|
||||
**Ziel:** Wenn alle Felder (Keys und Values) eines Records Konstanten sind, soll der gesamte Record in eine `BoundKind::Constant(Value::Record(...))` transformiert werden.
|
||||
|
||||
**Umsetzung:**
|
||||
- In `src/ast/compiler/optimizer/folder.rs` eine Methode `try_fold_record` implementieren.
|
||||
- In `src/ast/compiler/optimizer/engine.rs` im Case `BoundKind::Record` diese Methode aufrufen.
|
||||
- Dies ermöglicht es dem bereits existierenden Folder für `BoundKind::GetField`, Feldzugriffe auf Literalen direkt zur Kompilierzeit aufzulösen.
|
||||
|
||||
## 2. Record Inlining in SubstitutionMap
|
||||
|
||||
**Status:** Records werden derzeit nicht aggressiv in die `SubstitutionMap` aufgenommen, wenn sie Variablen zugewiesen werden.
|
||||
|
||||
**Ziel:** Zuweisungen von (konstanten) Records an Variablen sollen in `sub.values` gespeichert werden.
|
||||
|
||||
**Beispiel:**
|
||||
```lisp
|
||||
(def r {:x 10 :y 20})
|
||||
(.x r) ; Sollte zu 10 optimiert werden
|
||||
```
|
||||
|
||||
**Umsetzung:**
|
||||
- Sicherstellen, dass `Value::Record` von `Inliner::is_inlinable_value` als sicher eingestuft wird.
|
||||
- Testen, ob `BoundKind::GetField` bei einem `Get` auf eine Variable, die in `sub.values` als Record bekannt ist, den Wert extrahieren kann.
|
||||
|
||||
## 3. Propagation von Record-Layouts
|
||||
|
||||
**Ziel:** Wenn das Layout eines Records statisch bekannt ist (auch wenn die Werte nicht konstant sind), könnten Feldzugriffe (`GetField`) effizienter vorbereitet werden, um die Laufzeit-Suche im Layout-Hash/Index zu minimieren.
|
||||
|
||||
---
|
||||
*Erstellt am 26. Februar 2026 zur Nachverfolgung der Optimizer-Verbesserungen.*
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user