ASt Types

This commit is contained in:
Michael Schimmel
2025-10-25 15:58:18 +02:00
parent f2d9e1d9b0
commit a706483f28
5 changed files with 506 additions and 2 deletions
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -20,7 +20,8 @@ uses
Myc.Fmx.AstEditor.Workspace in 'Myc.Fmx.AstEditor.Workspace.pas',
Myc.Fmx.AstEditor.Text in 'Myc.Fmx.AstEditor.Text.pas',
Myc.Utils in '..\Src\Myc.Utils.pas',
Myc.Ast.Script in '..\Src\AST\Myc.Ast.Script.pas';
Myc.Ast.Script in '..\Src\AST\Myc.Ast.Script.pas',
Myc.Ast.Types in '..\Src\AST\Myc.Ast.Types.pas';
{$R *.res}
+1
View File
@@ -151,6 +151,7 @@
<DCCReference Include="Myc.Fmx.AstEditor.Text.pas"/>
<DCCReference Include="..\Src\Myc.Utils.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.Script.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.Types.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
+128
View File
@@ -0,0 +1,128 @@
---
# 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.
+374
View File
@@ -0,0 +1,374 @@
unit Myc.Ast.Types;
interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Data.Scalar;
type
// Defines the categories of types available in the language.
TStaticTypeKind = (
stUnknown, // Used during inference before a type is known
stVoid,
stOrdinal, // Int64
stFloat, // Double
stText,
stMethod,
stSeries,
stRecord,
stRecordSeries
);
TStaticTypeKindHelper = record helper for TStaticTypeKind
public
function ToString: string;
end;
// Represents a complete static type definition.
// This is a managed record, passed by value, cheap to copy.
TStaticType = record
public
type
// Forward declaration for mutual recursion
PStaticType = ^TStaticType;
// Defines the signature of a method
IMethodSignature = interface
{$region 'private'}
function GetParamTypes: TArray<TStaticType>;
function GetReturnType: TStaticType;
{$endregion}
property ParamTypes: TArray<TStaticType> read GetParamTypes;
property ReturnType: TStaticType read GetReturnType;
end;
public
Kind: TStaticTypeKind;
// Used if Kind = stMethod
Signature: IMethodSignature;
// Used if Kind = stSeries
ElementType: PStaticType;
// Used if Kind = stRecord or stRecordSeries
Definition: TScalarRecordDefinition;
class operator Initialize(out Dest: TStaticType);
class operator Finalize(var Dest: TStaticType);
// Factory functions
class function Create(AKind: TStaticTypeKind): TStaticType; static;
class function CreateSeries(const AElementType: TStaticType): TStaticType; static;
class function CreateMethod(const AParamTypes: TArray<TStaticType>; const AReturnType: TStaticType): TStaticType; static;
class function CreateRecord(const ADef: TScalarRecordDefinition): TStaticType; static;
class function CreateRecordSeries(const ADef: TScalarRecordDefinition): TStaticType; static;
class function FromScalarKind(AKind: TScalar.TKind): TStaticType; static;
function ToString: string;
class operator Equal(const A, B: TStaticType): Boolean;
class operator NotEqual(A, B: TStaticType): Boolean; inline;
end;
ETypeException = class(Exception);
// Defines the rules of the type system.
TTypeRules = record
private
class function Promote(const A, B: TStaticType): TStaticType; static;
public
// Checks if a value of type 'Source' can be assigned to 'Target'.
class function CanAssign(const Target, Source: TStaticType): Boolean; static;
// Determines the result type of a binary operation.
// Raises ETypeException on failure.
class function ResolveBinaryOp(Op: TScalar.TBinaryOp; const Left, Right: TStaticType): TStaticType; static;
// Determines the result type of a unary operation.
class function ResolveUnaryOp(Op: TScalar.TUnaryOp; const Right: TStaticType): TStaticType; static;
end;
implementation
uses
System.Generics.Defaults;
type
TMethodSignature = class(TInterfacedObject, TStaticType.IMethodSignature)
private
FParamTypes: TArray<TStaticType>;
FReturnType: TStaticType;
function GetParamTypes: TArray<TStaticType>;
function GetReturnType: TStaticType;
public
constructor Create(const AParamTypes: TArray<TStaticType>; const AReturnType: TStaticType);
end;
{ TMethodSignature }
constructor TMethodSignature.Create(const AParamTypes: TArray<TStaticType>; const AReturnType: TStaticType);
begin
inherited Create;
FParamTypes := AParamTypes;
FReturnType := AReturnType;
end;
function TMethodSignature.GetParamTypes: TArray<TStaticType>;
begin
Result := FParamTypes;
end;
function TMethodSignature.GetReturnType: TStaticType;
begin
Result := FReturnType;
end;
{ TStaticTypeKindHelper }
function TStaticTypeKindHelper.ToString: string;
begin
case Self of
stUnknown: Result := 'Unknown';
stVoid: Result := 'Void';
stOrdinal: Result := 'Ordinal';
stFloat: Result := 'Float';
stText: Result := 'Text';
stMethod: Result := 'Method';
stSeries: Result := 'Series';
stRecord: Result := 'Record';
stRecordSeries: Result := 'RecordSeries';
else
Result := 'ErrorType';
end;
end;
{ TStaticType }
class operator TStaticType.Initialize(out Dest: TStaticType);
begin
Dest.Kind := stUnknown;
Dest.Signature := nil;
Dest.ElementType := nil;
end;
class operator TStaticType.Finalize(var Dest: TStaticType);
begin
Dest.Signature := nil;
if Dest.ElementType <> nil then
begin
Finalize(Dest.ElementType^);
FreeMem(Dest.ElementType);
Dest.ElementType := nil;
end;
// TScalarRecordDefinition (FFields) is not owned by this record.
end;
class operator TStaticType.Equal(const A, B: TStaticType): Boolean;
var
i: Integer;
begin
if A.Kind <> B.Kind then
exit(False);
case A.Kind of
stSeries: Result := A.ElementType^ = B.ElementType^;
stRecord, stRecordSeries:
begin
if Length(A.Definition.Fields) <> Length(B.Definition.Fields) then
exit(False);
for i := 0 to High(A.Definition.Fields) do
begin
if (A.Definition.Fields[i].Name <> B.Definition.Fields[i].Name)
or (A.Definition.Fields[i].Kind <> B.Definition.Fields[i].Kind) then
exit(False);
end;
Result := True;
end;
stMethod:
begin
if A.Signature.ReturnType <> B.Signature.ReturnType then
exit(False);
if Length(A.Signature.ParamTypes) <> Length(B.Signature.ParamTypes) then
exit(False);
for i := 0 to High(A.Signature.ParamTypes) do
begin
if A.Signature.ParamTypes[i] <> B.Signature.ParamTypes[i] then
exit(False);
end;
Result := True;
end;
else
// stUnknown, stVoid, stOrdinal, stFloat, stText are equal if their kinds are equal.
Result := True;
end;
end;
class function TStaticType.Create(AKind: TStaticTypeKind): TStaticType;
begin
Result.Kind := AKind;
end;
class function TStaticType.CreateMethod(const AParamTypes: TArray<TStaticType>; const AReturnType: TStaticType): TStaticType;
begin
Result.Kind := stMethod;
Result.Signature := TMethodSignature.Create(AParamTypes, AReturnType);
end;
class function TStaticType.CreateRecord(const ADef: TScalarRecordDefinition): TStaticType;
begin
Result.Kind := stRecord;
Result.Definition := ADef;
end;
class function TStaticType.CreateRecordSeries(const ADef: TScalarRecordDefinition): TStaticType;
begin
Result.Kind := stRecordSeries;
Result.Definition := ADef;
end;
class function TStaticType.CreateSeries(const AElementType: TStaticType): TStaticType;
begin
Result.Kind := stSeries;
New(Result.ElementType);
Result.ElementType^ := AElementType;
end;
class function TStaticType.FromScalarKind(AKind: TScalar.TKind): TStaticType;
begin
case AKind of
TScalar.TKind.Ordinal: Result.Kind := stOrdinal;
TScalar.TKind.Float: Result.Kind := stFloat;
else
raise ETypeException.Create('Cannot convert invalid TScalar.TKind to TStaticType.');
end;
end;
function TStaticType.ToString: string;
var
i: Integer;
paramStr: string;
begin
Result := Kind.ToString;
case Kind of
stSeries: Result := 'Series<' + ElementType^.ToString + '>';
stRecord, stRecordSeries:
begin
Result := Result + '{';
for i := 0 to High(Definition.Fields) do
begin
Result := Result + Definition.Fields[i].Name + ': ' + Definition.Fields[i].Kind.ToString;
if i < High(Definition.Fields) then
Result := Result + ', ';
end;
Result := Result + '}';
end;
stMethod:
begin
paramStr := '';
for i := 0 to High(Signature.ParamTypes) do
begin
paramStr := paramStr + Signature.ParamTypes[i].ToString;
if i < High(Signature.ParamTypes) then
paramStr := paramStr + ', ';
end;
Result := Format('Method(%s): %s', [paramStr, Signature.ReturnType.ToString]);
end;
end;
end;
class operator TStaticType.NotEqual(A, B: TStaticType): Boolean;
begin
Result := not (A = B);
end;
{ TTypeRules }
class function TTypeRules.CanAssign(const Target, Source: TStaticType): Boolean;
begin
if Target = Source then
exit(True);
// Allow assigning an integer (Ordinal) to a float variable.
if (Target.Kind = stFloat) and (Source.Kind = stOrdinal) then
exit(True);
// TODO: Implement full assignment compatibility rules (e.g., for records/series)
Result := False;
end;
class function TTypeRules.Promote(const A, B: TStaticType): TStaticType;
begin
// Numeric promotion rule: Float wins
if (A.Kind = stFloat) and (B.Kind = stFloat) then
exit(TStaticType.Create(stFloat));
if (A.Kind = stFloat) and (B.Kind = stOrdinal) then
exit(TStaticType.Create(stFloat));
if (A.Kind = stOrdinal) and (B.Kind = stFloat) then
exit(TStaticType.Create(stFloat));
if (A.Kind = stOrdinal) and (B.Kind = stOrdinal) then
exit(TStaticType.Create(stOrdinal));
raise ETypeException.CreateFmt('Cannot promote types %s and %s', [A.ToString, B.ToString]);
end;
class function TTypeRules.ResolveBinaryOp(Op: TScalar.TBinaryOp; const Left, Right: TStaticType): TStaticType;
begin
case Op of
TScalar.TBinaryOp.Add, TScalar.TBinaryOp.Subtract, TScalar.TBinaryOp.Multiply:
begin
// Numeric operations: Promote and return
if (Left.Kind in [stOrdinal, stFloat]) and (Right.Kind in [stOrdinal, stFloat]) then
Result := Promote(Left, Right)
else
raise ETypeException.CreateFmt('Operator %s cannot be applied to %s and %s', [Op.ToString, Left.ToString, Right.ToString]);
end;
TScalar.TBinaryOp.Divide:
begin
// Division *always* results in Float, even Ordinal / Ordinal
if (Left.Kind in [stOrdinal, stFloat]) and (Right.Kind in [stOrdinal, stFloat]) then
Result := TStaticType.Create(stFloat)
else
raise ETypeException.CreateFmt('Operator %s cannot be applied to %s and %s', [Op.ToString, Left.ToString, Right.ToString]);
end;
TScalar.TBinaryOp.Equal,
TScalar.TBinaryOp.NotEqual,
TScalar.TBinaryOp.Less,
TScalar.TBinaryOp.Greater,
TScalar.TBinaryOp.LessOrEqual,
TScalar.TBinaryOp.GreaterOrEqual:
begin
// Comparison operations: Check if promotion is possible, but always return Ordinal (boolean)
if (Left.Kind in [stOrdinal, stFloat]) and (Right.Kind in [stOrdinal, stFloat]) then
Result := TStaticType.Create(stOrdinal)
else
raise ETypeException
.CreateFmt('Comparison operator %s cannot be applied to %s and %s', [Op.ToString, Left.ToString, Right.ToString]);
end;
else
raise ETypeException.Create('Unknown binary operator for type resolution.');
end;
end;
class function TTypeRules.ResolveUnaryOp(Op: TScalar.TUnaryOp; const Right: TStaticType): TStaticType;
begin
case Op of
TScalar.TUnaryOp.Negate:
begin
if not (Right.Kind in [stOrdinal, stFloat]) then
raise ETypeException.CreateFmt('Unary negation cannot be applied to %s', [Right.ToString]);
Result := Right; // Negation preserves type
end;
TScalar.TUnaryOp.Not:
begin
if not (Right.Kind = stOrdinal) then
raise ETypeException.CreateFmt('Logical not cannot be applied to %s', [Right.ToString]);
Result := TStaticType.Create(stOrdinal);
end;
else
raise ETypeException.Create('Unknown unary operator for type resolution.');
end;
end;
end.