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
+118
View File
@@ -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`)
-55
View File
@@ -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.
+37
View File
@@ -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.
+21
View File
@@ -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
)