From 83324a18927c210e0e76983d4aa637937a8cbe93 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Fri, 27 Feb 2026 08:58:38 +0100 Subject: [PATCH] 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` 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. --- docs/AST_Node_Structure.md | 118 +++++++++++++++++++++ docs/record_optimizations_plan.md | 55 ---------- docs/todo/record_optimizations_plan.md | 37 +++++++ docs/todo/record_optimizations_plan_soa.md | 73 +++++++++++++ examples/record_optimizations.myc | 21 ++++ 5 files changed, 249 insertions(+), 55 deletions(-) create mode 100644 docs/AST_Node_Structure.md delete mode 100644 docs/record_optimizations_plan.md create mode 100644 docs/todo/record_optimizations_plan.md create mode 100644 docs/todo/record_optimizations_plan_soa.md create mode 100644 examples/record_optimizations.myc diff --git a/docs/AST_Node_Structure.md b/docs/AST_Node_Structure.md new file mode 100644 index 0000000..34eef68 --- /dev/null +++ b/docs/AST_Node_Structure.md @@ -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` 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 { + <> + Identifier(Symbol) + Parameter(Symbol) + Def(Target, Value) + Assign(Target, Value) + MacroDecl(...) + Template(...) + Splice(...) + ... + } + + class BoundKind~T~ { + <> + Get(Address, Symbol) + Set(Address, Value) + Define(Symbol, Address, Kind, Value, Captures) + GetField(Record, Keyword) + Destructure(Pattern, Value) + Lambda(Params, Upvalues, Body) + ... + } + + class StaticType { + <> + // Type Information + } + + class NodeMetrics { + <> + +Rc~TypedNode~ original + +Purity purity + +bool is_recursive + } + + %% Typedefs und Spezialisierungen + class UntypedNode { + <> + Node~UntypedKind, ()~ + } + + class BoundNode { + <> + Node~BoundKind~()~, ()~ + } + + class TypedNode { + <> + Node~BoundKind~StaticType~, StaticType~ + } + + class AnalyzedNode { + <> + 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` (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, ()>` + * **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, 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, 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`) diff --git a/docs/record_optimizations_plan.md b/docs/record_optimizations_plan.md deleted file mode 100644 index f8b77bd..0000000 --- a/docs/record_optimizations_plan.md +++ /dev/null @@ -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`. -- **Interning:** Managed via a global `OnceLock>>>`. -- **FMap Optimization:** - - Stores `fields: Vec<(Keyword, StaticType)>`. - - Maintains a lookup array `Vec` mapping `Keyword.idx - first_key` to the index in the fields array. - - Provides `index_of(Keyword) -> Option` with **O(1)** complexity. -- **Thread Safety:** Uses `Arc` for global sharing across script threads. - -### Value & StaticType Updates -- `Value::Record` points to an `Arc` and a flat `Rc>`. -- `Value::FieldAccessor(Keyword)` added as a first-class callable value (e.g., `.name`). -- `StaticType::Record` now holds `Arc`. - -## 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. diff --git a/docs/todo/record_optimizations_plan.md b/docs/todo/record_optimizations_plan.md new file mode 100644 index 0000000..e50f2db --- /dev/null +++ b/docs/todo/record_optimizations_plan.md @@ -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.* diff --git a/docs/todo/record_optimizations_plan_soa.md b/docs/todo/record_optimizations_plan_soa.md new file mode 100644 index 0000000..a7f6513 --- /dev/null +++ b/docs/todo/record_optimizations_plan_soa.md @@ -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, Rc>), + // ... +} +``` + +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>`. 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`) speichert intern keine `Value`s, sondern nutzt ein natives Array (z. B. einen Ringpuffer): `Vec`. + +### 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, // Perfekt sequenziell im Cache + volumes: Vec, +} +``` + +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)` 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. \ No newline at end of file diff --git a/examples/record_optimizations.myc b/examples/record_optimizations.myc new file mode 100644 index 0000000..802bc3e --- /dev/null +++ b/examples/record_optimizations.myc @@ -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 +)