Files
RustAst/docs/Single-Map-Arguments 1.md
Michael Schimmel 494bf554d2 Old Docs added
2026-02-20 10:09:22 +01:00

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:

  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-IAstNodes) 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.