128 lines
9.2 KiB
Markdown
128 lines
9.2 KiB
Markdown
|
|
---
|
|
|
|
# Projektplan: "Single-Map-Argument" (SMA) Architektur
|
|
|
|
*Datum: 23. Oktober 2025*
|
|
|
|
## 1. Motivation: Die "Visuelle Sprache"
|
|
|
|
Das primäre Entwicklungsziel ist **nicht** eine Text-basierte Programmiersprache, sondern ein **visueller Editor**, in dem Logik durch das Kombinieren von Blöcken erstellt wird. Der Text-Parser (`Myc.Ast.Script`) ist lediglich eine sekundäre Repräsentation.
|
|
|
|
Für einen visuellen Editor ist syntaktische Komplexität Gift. Jede Ausnahme, jedes Schlüsselwort und jede alternative Syntax (wie `[]` vs. `()`) erfordert einen neuen, speziellen visuellen Block, was die Benutzeroberfläche überlädt und die Konsistenz bricht.
|
|
|
|
Das ultimative Ziel ist eine **100% einheitliche Syntax**, bei der es nur noch *eine* Art gibt, eine Operation auszudrücken: den Funktionsaufruf. In einer S-Expression-Welt ist die konsistenteste Form dafür `(Funktionsname Argumente...)`.
|
|
|
|
## 2. Ziel: Das "Single-Map-Argument" (SMA) Modell
|
|
|
|
Um die visuelle Darstellung auf das absolute Minimum zu reduzieren (ein Block für "Funktion" und ein Slot für "Argumente"), führen wir das **"Single-Map-Argument" (SMA) Modell** ein.
|
|
|
|
Es gibt nur noch *eine* gültige Aufrufkonvention:
|
|
`(Funktionsname {Argument-Map})`
|
|
|
|
Jede Funktion, egal ob nativ (`+`) oder benutzerdefiniert (`my-lambda`), wird mit einem einzigen Argument aufgerufen: einem **Map-Literal** (das zu einem `TScalarRecord` ausgewertet wird). Dieses Literal definiert die Argumente über Key-Value-Paare.
|
|
|
|
**Beispiele:**
|
|
|
|
* **Arithmetik:** `(+ {:x 1 :y 2})`
|
|
* **RTL-Funktion:** `(Abs {:value -10})`
|
|
* **Lambda-Definition:** `(fn my-adder ({:x1 1 :x2 2}) ...)`
|
|
* **Lambda-Aufruf:** `(my-adder {:x1 5})`
|
|
|
|
Dieses Design erfüllt die Anforderung an die visuelle Konsistenz perfekt. Ein "Aufruf"-Block im Editor hat immer nur zwei definierte Slots: den Namen (z.B. `+`) und ein Map-Literal (z.B. `{:x ..., :y ...}`).
|
|
|
|
## 3. Analyse & Performance-Strategie
|
|
|
|
Unsere Diskussion hat ergeben, dass dieses Modell zwar visuell perfekt, aber in einer naiven Implementierung performancetechnisch inakzeptabel wäre.
|
|
|
|
### Das Problem: Die naive Implementierung (Verworfen)
|
|
|
|
Eine naive Implementierung würde jeden Aufruf zur Laufzeit gleich behandeln:
|
|
1. Der `TEvaluator` wertet das Map-Literal `{:x 1 :y 2}` zu einem vollwertigen `TScalarRecord` aus (Heap-Allokation, Füllen einer Map/Dictionary-Struktur).
|
|
2. Er ruft die native `+` Funktion mit diesem *einen* `TScalarRecord`-Argument auf.
|
|
3. Die `+` Funktion (bzw. ihr Wrapper) müsste die Map parsen, die Keys `:x` und `:y` nachschlagen und die Werte extrahieren.
|
|
|
|
Für Operationen wie `+`, die millionenfach pro Sekunde aufgerufen werden, ist dieser Overhead (Heap-Allokation + Hashmap-Lookups) katastrophal und ein absoluter Showstopper.
|
|
|
|
### Die Lösung: Kompilierung von Aufrufsignaturen
|
|
|
|
Wir vermeiden diesen Overhead, indem wir den **`TAstBinder` (den "Compiler")** die "Dekonstruktion" der Argument-Maps zur Compile-Zeit durchführen lassen.
|
|
|
|
Wir implementieren eine **Drei-Pfade-Kompilierung** für `VisitFunctionCall`:
|
|
|
|
#### Pfad 1: "Fast Path" (Nativ / RTL)
|
|
|
|
Dieser Pfad optimiert alle Aufrufe an bekannte, fest verdrahtete RTL-Funktionen.
|
|
|
|
* **Aktion (Binder):**
|
|
1. Der `TAstBinder` erhält ein **statisches Registry nativer Signaturen** (z.B. `TDictionary<string, TArray<string>>`). Dieses mappt Namen auf *geordnete* Key-Listen: `'+' -> [':x', ':y']`, `'Abs' -> [':value']`.
|
|
2. Bei `(sub {:a 5 :b 3})` schlägt er `sub` nach. Treffer! Signatur ist `[':a', ':b']`.
|
|
3. Der Binder validiert das `IMapLiteralNode` (alle Keys da? unbekannte Keys?).
|
|
4. **Argument-Umschreibung (Rewriting):** Der Binder *ignoriert* die Map-Struktur und erzeugt einen neuen, *positionalen* `TArray<IAstNode>`: `[ (Node 5), (Node 3) ]`.
|
|
5. Er erzeugt einen `TBoundFunctionCallNode`, der auf die native `sub`-Funktion zeigt, aber das *neue positionale Array* als Argumentenliste enthält.
|
|
* **Aktion (Evaluator):**
|
|
1. Der Evaluator sieht einen normalen, positionalen Aufruf.
|
|
2. Er wertet die Argumente `5` und `3` aus und ruft die `TRtlFunctions.Subtract` direkt mit einem `TArray<TDataValue>` auf.
|
|
* **Ergebnis:** Keinerlei Map-Allokation zur Laufzeit. Maximale Performance.
|
|
|
|
#### Pfad 2: "Fast Path" (Direkte Lambda)
|
|
|
|
Dieser Pfad optimiert direkte Aufrufe an Lambdas, deren Definition dem Binder bereits bekannt ist.
|
|
|
|
* **Aktion (Binder):**
|
|
1. Bei `(fn my-adder ({:x1 1 :x2 2}) ...)` parst der Binder die Signatur `({:x1 1, :x2 2})`.
|
|
2. Er speichert diese Signatur-Metadaten (Keys und Default-Wert-Nodes) im `IScopeDescriptor` als Metadatum für die Variable `my-adder`.
|
|
3. Bei einem späteren Aufruf `(my-adder {:x1 5})`:
|
|
4. Der Binder schlägt `my-adder` im Scope nach. Treffer! Er findet die Variable *und* die gespeicherte Signatur.
|
|
5. **Argument-Umschreibung:** Er führt die *gleiche* Optimierung wie bei nativen Aufrufen durch. Er parst das `IMapLiteralNode`, füllt fehlende Keys mit den Default-Nodes (z.B. `:x2` -> `(Node 2)`) und erzeugt einen positionalen `TArray<IAstNode>`: `[ (Node 5), (Node 2) ]`.
|
|
6. Er erzeugt einen `TBoundFunctionCallNode`, der das positionale Array enthält.
|
|
* **Aktion (Evaluator):**
|
|
1. Der Evaluator wertet die Closure `my-adder` aus.
|
|
2. Er wertet die Argumente `5` und `2` aus.
|
|
3. Er ruft die Closure mit einem *positionalen* `TArray<TDataValue>` auf.
|
|
* **Ergebnis:** Auch hier: Keinerlei Map-Allokation zur Laufzeit.
|
|
|
|
#### Pfad 3: "Slow Path" (Dynamisch / Polymorph)
|
|
|
|
Dies ist der Fallback für alle Aufrufe, die der Binder zur Compile-Zeit *unmöglich* auflösen kann.
|
|
|
|
* **Szenarien:**
|
|
* **Funktionen höherer Ordnung (HOFs):** `(Map my-series (fn ({:item}) ...))` -> Die `Map`-Funktion *muss* die Lambda dynamisch aufrufen.
|
|
* **Indirekte Aufrufe:** `(fn call-it (func) (func {:a 1}))` -> `func` ist zur Compile-Zeit unbekannt.
|
|
* **Späte Bindung / Forward-Deklarationen.**
|
|
* **Aktion (Binder):**
|
|
1. Der Binder kann die Signatur nicht finden.
|
|
2. Er kann *nicht* optimieren. Er behandelt das `IMapLiteralNode` `{:a 1}` als regulären Wert.
|
|
3. Er erzeugt einen `TBoundFunctionCallNode` mit einem Argumenten-Array, das *nur dieses eine* `IMapLiteralNode` enthält.
|
|
* **Aktion (Evaluator):**
|
|
1. Der Evaluator *muss* nun das `IMapLiteralNode` zu einem echten `TScalarRecord` auswerten (der "Overhead" entsteht hier).
|
|
2. Er ruft die Closure (z.B. `func`) mit *einem* Argument auf (dem `TScalarRecord`).
|
|
3. Die Closure selbst muss die **Laufzeit-Destrukturierung** durchführen: Sie parst die Map, extrahiert die Keys (`:a`) und wendet ihre Defaults an.
|
|
* **Ergebnis:** Das System bleibt voll funktionsfähig und konsistent, nutzt aber den langsameren Pfad nur, wenn es semantisch unvermeidbar ist.
|
|
|
|
---
|
|
|
|
## 4. TODOs (Detailliert)
|
|
|
|
1. **Parser (`Myc.Ast.Script`)**
|
|
* [ ] `TLexer`: `tkLeftBracket`, `tkRightBracket` entfernen. `tkLBrace` (`{`), `tkRBrace` (`}`), `tkColon` (`:`) hinzufügen.
|
|
* [ ] `IAstNode`: `IKeywordNode` (für `:key`) und `IMapLiteralNode` (enthält `TArray<TPair<IKeywordNode, IAstNode>>`) definieren.
|
|
* [ ] `TParser`: `ParseExpression` erweitern, um `{...}` zu `IMapLiteralNode` und `:...` zu `IKeywordNode` zu parsen.
|
|
* [ ] `TParser`: `ParseList` anpassen. Die `fn`- und `defmacro`-Logik muss die Parameterliste jetzt als `IMapLiteralNode` (für die Destrukturierung) statt einer Liste von Identifiern parsen.
|
|
|
|
2. **Binder (`Myc.Ast.Binding`)**
|
|
* [ ] `TAstBinder.Create`: Das **Native Signature Registry** (`TDictionary<string, TArray<string>>`) initialisieren.
|
|
* [ ] `TAstBinder.VisitLambdaExpression`: Die `IMapLiteralNode`-Signatur parsen. Die Metadaten (geordnete Keys und Default-`IAstNode`s) müssen in der `TBoundLambdaExpressionNode` oder im `IScopeDescriptor` gespeichert werden.
|
|
* [ ] `TAstBinder.VisitFunctionCall`: Die **Kern-Drei-Pfade-Logik** implementieren:
|
|
1. Lookup im Native Registry (Pfad 1).
|
|
2. Lookup im `IScopeDescriptor` nach Lambda-Metadaten (Pfad 2).
|
|
3. Fallback auf "Slow Path" (Pfad 3).
|
|
* [ ] `TAstBinder`: Eine private `function RewriteArguments(const Signature: ...; const CallMap: IMapLiteralNode): TArray<IAstNode>` implementieren, die die "Fast Path"-Dekonstruktion durchführt.
|
|
|
|
3. **Evaluator (`Myc.Ast.Evaluator`)**
|
|
* [ ] `TEvaluatorVisitor`: `VisitMapLiteral` implementieren. Diese Methode wertet ein `IMapLiteralNode` zu einem `TDataValue (vkRecord)` aus (der "Slow Path"-Overhead).
|
|
* [ ] `TEvaluatorVisitor.VisitLambdaExpression`: Die *Laufzeit*-Destrukturierungslogik implementieren. Diese wird aktiv, wenn die Closure (TFunc) mit einem `TDataValue (vkRecord)` (Pfad 3) anstelle eines `TArray<TDataValue>` (Pfad 2) aufgerufen wird. (Benötigt Anpassung der Closure-Signatur oder -Logik).
|
|
|
|
4. **RTL (Anpassung & Registrierung)**
|
|
* [ ] `Myc.Ast.RTL.Core`: Die Implementierungen (`TRtlFunctions.Add` etc.) bleiben **unverändert**. Sie erwarten weiterhin `TArray<TDataValue>`, da der Binder für sie übersetzt.
|
|
* [ ] `Myc.Ast.RTL`: Die RTTI-Registrierung muss erweitert werden, um dem Binder die Signaturen bereitzustellen. `TRtlFunctionAttribute` könnte erweitert werden: `[TRtlFunction('+', ':x,:y')]`. Diese Infos füllen das Native Registry im Binder. |