9.2 KiB
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:
- Der
TEvaluatorwertet das Map-Literal{:x 1 :y 2}zu einem vollwertigenTScalarRecordaus (Heap-Allokation, Füllen einer Map/Dictionary-Struktur). - Er ruft die native
+Funktion mit diesem einenTScalarRecord-Argument auf. - Die
+Funktion (bzw. ihr Wrapper) müsste die Map parsen, die Keys:xund:ynachschlagen 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):
- Der
TAstBindererhält ein statisches Registry nativer Signaturen (z.B.TDictionary<string, TArray<string>>). Dieses mappt Namen auf geordnete Key-Listen:'+' -> [':x', ':y'],'Abs' -> [':value']. - Bei
(sub {:a 5 :b 3})schlägt ersubnach. Treffer! Signatur ist[':a', ':b']. - Der Binder validiert das
IMapLiteralNode(alle Keys da? unbekannte Keys?). - Argument-Umschreibung (Rewriting): Der Binder ignoriert die Map-Struktur und erzeugt einen neuen, positionalen
TArray<IAstNode>:[ (Node 5), (Node 3) ]. - Er erzeugt einen
TBoundFunctionCallNode, der auf die nativesub-Funktion zeigt, aber das neue positionale Array als Argumentenliste enthält.
- Der
- Aktion (Evaluator):
- Der Evaluator sieht einen normalen, positionalen Aufruf.
- Er wertet die Argumente
5und3aus und ruft dieTRtlFunctions.Subtractdirekt mit einemTArray<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):
- Bei
(fn my-adder ({:x1 1 :x2 2}) ...)parst der Binder die Signatur({:x1 1, :x2 2}). - Er speichert diese Signatur-Metadaten (Keys und Default-Wert-Nodes) im
IScopeDescriptorals Metadatum für die Variablemy-adder. - Bei einem späteren Aufruf
(my-adder {:x1 5}): - Der Binder schlägt
my-adderim Scope nach. Treffer! Er findet die Variable und die gespeicherte Signatur. - 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 positionalenTArray<IAstNode>:[ (Node 5), (Node 2) ]. - Er erzeugt einen
TBoundFunctionCallNode, der das positionale Array enthält.
- Bei
- Aktion (Evaluator):
- Der Evaluator wertet die Closure
my-adderaus. - Er wertet die Argumente
5und2aus. - Er ruft die Closure mit einem positionalen
TArray<TDataValue>auf.
- Der Evaluator wertet die Closure
- 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}) ...))-> DieMap-Funktion muss die Lambda dynamisch aufrufen. - Indirekte Aufrufe:
(fn call-it (func) (func {:a 1}))->funcist zur Compile-Zeit unbekannt. - Späte Bindung / Forward-Deklarationen.
- Funktionen höherer Ordnung (HOFs):
- Aktion (Binder):
- Der Binder kann die Signatur nicht finden.
- Er kann nicht optimieren. Er behandelt das
IMapLiteralNode{:a 1}als regulären Wert. - Er erzeugt einen
TBoundFunctionCallNodemit einem Argumenten-Array, das nur dieses eineIMapLiteralNodeenthält.
- Aktion (Evaluator):
- Der Evaluator muss nun das
IMapLiteralNodezu einem echtenTScalarRecordauswerten (der "Overhead" entsteht hier). - Er ruft die Closure (z.B.
func) mit einem Argument auf (demTScalarRecord). - Die Closure selbst muss die Laufzeit-Destrukturierung durchführen: Sie parst die Map, extrahiert die Keys (
:a) und wendet ihre Defaults an.
- Der Evaluator muss nun das
- Ergebnis: Das System bleibt voll funktionsfähig und konsistent, nutzt aber den langsameren Pfad nur, wenn es semantisch unvermeidbar ist.
4. TODOs (Detailliert)
-
Parser (
Myc.Ast.Script)TLexer:tkLeftBracket,tkRightBracketentfernen.tkLBrace({),tkRBrace(}),tkColon(:) hinzufügen.IAstNode:IKeywordNode(für:key) undIMapLiteralNode(enthältTArray<TPair<IKeywordNode, IAstNode>>) definieren.TParser:ParseExpressionerweitern, um{...}zuIMapLiteralNodeund:...zuIKeywordNodezu parsen.TParser:ParseListanpassen. Diefn- unddefmacro-Logik muss die Parameterliste jetzt alsIMapLiteralNode(für die Destrukturierung) statt einer Liste von Identifiern parsen.
-
Binder (
Myc.Ast.Binding)TAstBinder.Create: Das Native Signature Registry (TDictionary<string, TArray<string>>) initialisieren.TAstBinder.VisitLambdaExpression: DieIMapLiteralNode-Signatur parsen. Die Metadaten (geordnete Keys und Default-IAstNodes) müssen in derTBoundLambdaExpressionNodeoder imIScopeDescriptorgespeichert werden.TAstBinder.VisitFunctionCall: Die Kern-Drei-Pfade-Logik implementieren:- Lookup im Native Registry (Pfad 1).
- Lookup im
IScopeDescriptornach Lambda-Metadaten (Pfad 2). - Fallback auf "Slow Path" (Pfad 3).
TAstBinder: Eine privatefunction RewriteArguments(const Signature: ...; const CallMap: IMapLiteralNode): TArray<IAstNode>implementieren, die die "Fast Path"-Dekonstruktion durchführt.
-
Evaluator (
Myc.Ast.Evaluator)TEvaluatorVisitor:VisitMapLiteralimplementieren. Diese Methode wertet einIMapLiteralNodezu einemTDataValue (vkRecord)aus (der "Slow Path"-Overhead).TEvaluatorVisitor.VisitLambdaExpression: Die Laufzeit-Destrukturierungslogik implementieren. Diese wird aktiv, wenn die Closure (TFunc) mit einemTDataValue (vkRecord)(Pfad 3) anstelle einesTArray<TDataValue>(Pfad 2) aufgerufen wird. (Benötigt Anpassung der Closure-Signatur oder -Logik).
-
RTL (Anpassung & Registrierung)
Myc.Ast.RTL.Core: Die Implementierungen (TRtlFunctions.Addetc.) bleiben unverändert. Sie erwarten weiterhinTArray<TDataValue>, da der Binder für sie übersetzt.Myc.Ast.RTL: Die RTTI-Registrierung muss erweitert werden, um dem Binder die Signaturen bereitzustellen.TRtlFunctionAttributekönnte erweitert werden:[TRtlFunction('+', ':x,:y')]. Diese Infos füllen das Native Registry im Binder.