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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
;; Benchmark: 1.7ms
|
||||||
|
;; Benchmark-Repeat: 3
|
||||||
|
;; Benchmark: TBD
|
||||||
|
;; Tests the effect of record inlining and field lookup optimization
|
||||||
|
;; Output: 10000
|
||||||
|
(do
|
||||||
|
(macro while [cond body]
|
||||||
|
`(do
|
||||||
|
(def _while_loop (fn []
|
||||||
|
(if ~cond
|
||||||
|
(do ~body (_while_loop))
|
||||||
|
...)))
|
||||||
|
(_while_loop)))
|
||||||
|
|
||||||
|
(def config {:start 0 :limit 10000 :step 2})
|
||||||
|
(def x (.start config))
|
||||||
|
|
||||||
|
(while (< x (.limit config))
|
||||||
|
(assign x (+ x (.step config))))
|
||||||
|
x
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user