This commit is contained in:
Michael Schimmel
2025-10-30 20:27:44 +01:00
parent 798aa08f02
commit 0526ec8a24
17 changed files with 193 additions and 137 deletions
+30 -19
View File
@@ -259,8 +259,15 @@ begin
// externally evaluate the expression using the injected evaluator
value := FEvaluate(expr);
// Allow unquoting keywords
if value.Kind in [vkScalar, vkText, vkVoid, vkKeyword] then
Result := TDataValue.FromIntf<IAstNode>(TAst.Constant(value))
begin
// vkKeyword needs to be handled differently than other constants
if value.Kind = vkKeyword then
Result := TDataValue.FromIntf<IAstNode>(TAst.Keyword(value.AsKeyword.Name))
else
Result := TDataValue.FromIntf<IAstNode>(TAst.Constant(value));
end
else
raise Exception.CreateFmt('Cannot unquote complex value of type %s at compile time.', [value.Kind.ToString]);
end;
@@ -290,8 +297,8 @@ end;
function TExpansionVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): TDataValue;
begin
// This node type does not support splicing, so we use the default
// TAstTransformer implementation which just transforms child values.
// Record literals do not support splicing.
// We just use the default TAstTransformer implementation which visits child values.
Result := inherited VisitRecordLiteral(Node);
end;
@@ -500,6 +507,7 @@ var
i: Integer;
begin
// --- Transformation: Keyword-as-Function ---
// Check if the callee is a keyword literal
if (Node.Callee is TKeywordNode) then
begin
var keywordNode := (Node.Callee as TKeywordNode);
@@ -508,18 +516,17 @@ begin
// 1. Validate argument count
if Length(Node.Arguments) <> 1 then
raise ETypeException
.CreateFmt('Keyword :%s expects exactly one argument (the record), but got %d', [keywordName, Length(Node.Arguments)]);
.CreateFmt('Keyword :%s expects exactly one argument (the record/map), but got %d', [keywordName, Length(Node.Arguments)]);
// 2. Bind the base (the record)
// 2. Bind the base (the record/map)
FNextIsTail := False; // Accessing a member is not a tail call
var baseNode := Accept(Node.Arguments[0]).AsIntf<IAstNode>;
// 3. Create a synthetic IMemberAccessNode
var memberIdentifier := TAst.Identifier(keywordName);
var memberAccessNode := TAst.MemberAccess(baseNode, memberIdentifier);
var memberAccessNode := TAst.MemberAccess(baseNode, TAst.Keyword(keywordName));
// 4. Re-bind the synthetic node by calling Accept (which dispatches to VisitMemberAccess)
// This ensures type checking and type inference for member access is done in one place.
// This ensures type checking and type inference for member access is centralized.
Result := Accept(memberAccessNode);
exit;
end;
@@ -925,19 +932,19 @@ var
begin
baseNode := Accept(Node.Base).AsIntf<IAstNode>;
baseType := (baseNode as TAstNode).StaticType;
var memberName := Node.Member.Name;
elemType := TTypes.Unknown;
if (baseType.Kind <> TStaticTypeKind.stUnknown) then
begin
if (baseType.Kind <> TStaticTypeKind.stRecord) and (baseType.Kind <> TStaticTypeKind.stRecordSeries) then
raise ETypeException.CreateFmt('Member access `.` requires a record or record series, but got %s', [baseType.ToString]);
raise ETypeException.CreateFmt('Member access requires a record or record series, but got %s', [baseType.ToString]);
fieldIndex := baseType.Definition.IndexOf(memberName);
// Use IndexOf(string) which correctly delegates to IndexOf(IKeyword)
fieldIndex := baseType.Definition.IndexOf(Node.Member.Value);
if fieldIndex < 0 then
raise ETypeException.CreateFmt('Member "%s" not found in type %s', [memberName, baseType.ToString]);
raise ETypeException.CreateFmt('Member "%s" not found in type %s', [Node.Member.Value.Name, baseType.ToString]);
var fieldType := TTypes.FromScalarKind(baseType.Definition.Fields[fieldIndex].Kind);
var fieldType := TTypes.FromScalarKind(baseType.Definition.Fields[fieldIndex].Value);
if baseType.Kind = TStaticTypeKind.stRecord then
elemType := fieldType
@@ -953,7 +960,7 @@ function TAstBinder.VisitRecordLiteral(const Node: IRecordLiteralNode): TDataVal
var
i: Integer;
boundFields: TArray<TRecordFieldLiteral>;
defFields: TArray<TScalarRecordField>;
defFields: TArray<TScalarRecordDefinition.TField>;
def: TScalarRecordDefinition;
staticType: IStaticType;
boundNode: IRecordLiteralNode;
@@ -964,18 +971,21 @@ begin
SetLength(boundFields, Length(Node.Fields));
SetLength(defFields, Length(Node.Fields));
// We assume this is a TScalarRecord (Path A) until proven otherwise.
// The "dual path" logic for stDictionary is not yet implemented.
for i := 0 to High(Node.Fields) do
begin
valNode := Accept(Node.Fields[i].Value).AsIntf<IAstNode>;
valType := (valNode as TAstNode).StaticType;
// Records can only store scalar values
// Path A: Records can only store scalar values
if not (valType.Kind in [stOrdinal, stFloat]) then
raise ETypeException.CreateFmt(
'Record fields must be scalar (Ordinal or Float), but field "%s" is %s',
[Node.Fields[i].Name, valType.ToString]);
'Record fields must be scalar (Ordinal or Float), but field ":%s" is %s',
[Node.Fields[i].Key.Value.Name, valType.ToString]);
boundFields[i] := TRecordFieldLiteral.Create(Node.Fields[i].Name, valNode);
boundFields[i] := TRecordFieldLiteral.Create(Node.Fields[i].Key, valNode);
var scalarKind: TScalar.TKind;
if valType.Kind = stOrdinal then
@@ -983,7 +993,8 @@ begin
else
scalarKind := TScalar.TKind.Float;
defFields[i] := TScalarRecordField.Create(Node.Fields[i].Name, scalarKind);
// Create the definition field using the Keyword's name
defFields[i] := TScalarRecordField.Create(Node.Fields[i].Key.Value, scalarKind);
end;
def := TScalarRecordDefinition.Create(defFields);
+1 -1
View File
@@ -237,7 +237,7 @@ end;
function TDebugEvaluatorVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TDataValue;
begin
AppendLine(Format('MemberAccess (Member: %s) {', [Node.Member.Name]));
AppendLine(Format('MemberAccess (Member: %s) {', [Node.Member.Value.Name]));
Indent;
try
Result := inherited VisitMemberAccess(Node);
+1 -1
View File
@@ -449,7 +449,7 @@ begin
Indent;
for field in Node.Fields do
begin
LogFmt('Field :%s', [field.Name]);
LogFmt('Field :%s', [field.Key.Value.Name]);
Indent;
field.Value.Accept(Self);
Unindent;
+2 -4
View File
@@ -437,14 +437,12 @@ end;
function TEvaluatorVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TDataValue;
var
baseValue: TDataValue;
memberName: string;
begin
baseValue := Node.Base.Accept(Self);
memberName := Node.Member.Name;
case baseValue.Kind of
vkRecordSeries: Result := TDataValue.FromSeries(baseValue.AsRecordSeries.CreateMemberSeries(memberName));
vkRecord: Result := baseValue.AsRecord.Items[memberName];
vkRecordSeries: Result := TDataValue.FromSeries(baseValue.AsRecordSeries.CreateMemberSeries(Node.Member.Value));
vkRecord: Result := baseValue.AsRecord[Node.Member.Value];
else
raise EArgumentException.Create('Member access operator `.` is not supported for this value type.');
end;
+7 -3
View File
@@ -578,7 +578,7 @@ begin
field.Value.Accept(Self); // This pushes the value JSON onto the stack
fieldObj := TJSONObject.Create;
fieldObj.AddPair('Name', TJSONString.Create(field.Name));
fieldObj.AddPair('Name', TJSONString.Create(field.Key.Value.Name));
fieldObj.AddPair('Value', FJsonObjectStack.Pop);
fieldsArray.Add(fieldObj);
end;
@@ -900,7 +900,7 @@ end;
function TJsonAstConverter.JsonToMemberAccessNode(const AObj: TJSONObject): IMemberAccessNode;
begin
Result := TAst.MemberAccess(JsonToNode(AObj.GetValue('Base')), JsonToIdentifierNode(AObj.GetValue('Member') as TJSONObject));
Result := TAst.MemberAccess(JsonToNode(AObj.GetValue('Base')), JsonToKeywordNode(AObj.GetValue('Member') as TJSONObject));
end;
function TJsonAstConverter.JsonToRecordLiteralNode(const AObj: TJSONObject): IRecordLiteralNode;
@@ -915,7 +915,11 @@ begin
for i := 0 to fieldsArray.Count - 1 do
begin
fieldObj := fieldsArray.Items[i] as TJSONObject;
fields[i] := TRecordFieldLiteral.Create(fieldObj.GetValue<string>('Name'), JsonToNode(fieldObj.GetValue('Value')));
fields[i] :=
TRecordFieldLiteral.Create(
TKeywordNode.Create(TKeywordRegistry.Intern(fieldObj.GetValue<string>('Name'))),
JsonToNode(fieldObj.GetValue('Value'))
);
end;
Result := TAst.RecordLiteral(fields);
end;
+6 -6
View File
@@ -55,9 +55,9 @@ type
// A single field in a record literal
TRecordFieldLiteral = record
Name: string; // The field name (from keyword :name)
Key: IKeywordNode;
Value: IAstNode;
constructor Create(const AName: string; const AValue: IAstNode);
constructor Create(const AKey: IKeywordNode; const AValue: IAstNode);
end;
IAstVisitor = interface
@@ -264,10 +264,10 @@ type
IMemberAccessNode = interface(IAstNode)
{$region 'private'}
function GetBase: IAstNode;
function GetMember: IIdentifierNode;
function GetMember: IKeywordNode;
{$endregion}
property Base: IAstNode read GetBase;
property Member: IIdentifierNode read GetMember;
property Member: IKeywordNode read GetMember;
end;
// Represents a record literal, e.g., {:a 1 :b 2}
@@ -335,9 +335,9 @@ end;
{ TRecordFieldLiteral }
constructor TRecordFieldLiteral.Create(const AName: string; const AValue: IAstNode);
constructor TRecordFieldLiteral.Create(const AKey: IKeywordNode; const AValue: IAstNode);
begin
Name := AName;
Key := AKey;
Value := AValue;
end;
+3 -3
View File
@@ -376,7 +376,7 @@ begin
raise Exception.Create('Syntax Error: Missing value for key ' + fieldName + ' in record literal.');
fieldValue := ParseExpression.Node;
fields.Add(TRecordFieldLiteral.Create(fieldName, fieldValue));
fields.Add(TRecordFieldLiteral.Create(TKeywordNode.Create(TKeywordRegistry.Intern(fieldName)), fieldValue));
end;
Result := TAst.RecordLiteral(fields.ToArray);
@@ -490,7 +490,7 @@ begin
else if SameText(head.Token.Text, 'get') then
Result := TAst.Indexer(tailNodes[0], tailNodes[1])
else if (Length(head.Token.Text) > 1) and (head.Token.Text.StartsWith('.')) then
Result := TAst.MemberAccess(tailNodes[0], TAst.Identifier(head.Token.Text.Substring(1)));
Result := TAst.MemberAccess(tailNodes[0], TAst.Keyword(head.Token.Text.Substring(1)));
end;
// If no special form matched, treat it as a generic function call.
@@ -916,7 +916,7 @@ begin
for field in Node.Fields do
begin
NewLine;
Append(':' + field.Name);
Append(':' + field.Key.Value.Name);
Append(' ');
field.Value.Accept(Self);
end;
+3 -2
View File
@@ -386,7 +386,8 @@ begin
for i := 0 to High(Self.FDefinition.Fields) do
begin
if (Self.FDefinition.Fields[i].Name <> otherDef.Fields[i].Name) or (Self.FDefinition.Fields[i].Kind <> otherDef.Fields[i].Kind) then
// Use fast interface comparison for Keys
if (Self.FDefinition.Fields[i].Key <> otherDef.Fields[i].Key) or (Self.FDefinition.Fields[i].Value <> otherDef.Fields[i].Value) then
exit(False);
end;
@@ -400,7 +401,7 @@ begin
Result := GetKind.ToString + '{';
for i := 0 to High(FDefinition.Fields) do
begin
Result := Result + FDefinition.Fields[i].Name + ': ' + FDefinition.Fields[i].Kind.ToString;
Result := Result + FDefinition.Fields[i].Key.Name + ': ' + FDefinition.Fields[i].Value.ToString;
if i < High(FDefinition.Fields) then
Result := Result + ', ';
end;
+7 -3
View File
@@ -319,7 +319,7 @@ end;
function TAstTransformer.VisitMemberAccess(const Node: IMemberAccessNode): TDataValue;
begin
var base := Accept(Node.Base).AsIntf<IAstNode>;
var member := Node.Member;
var member := Accept(Node.Member).AsIntf<IKeywordNode>;
if (base = Node.Base) and (member = Node.Member) then
Result := TDataValue.FromIntf<IMemberAccessNode>(Node)
else
@@ -331,6 +331,7 @@ var
i: Integer;
hasChanged: Boolean;
newFields: TArray<TRecordFieldLiteral>;
newKey: IKeywordNode;
newValue: IAstNode;
begin
hasChanged := False;
@@ -338,10 +339,13 @@ begin
for i := 0 to High(Node.Fields) do
begin
newKey := Accept(Node.Fields[i].Key).AsIntf<IKeywordNode>;
newValue := Accept(Node.Fields[i].Value).AsIntf<IAstNode>;
if newValue <> Node.Fields[i].Value then
if (newKey <> Node.Fields[i].Key) or (newValue <> Node.Fields[i].Value) then
hasChanged := True;
newFields[i] := TRecordFieldLiteral.Create(Node.Fields[i].Name, newValue);
newFields[i] := TRecordFieldLiteral.Create(newKey, newValue);
end;
if hasChanged then
+15 -13
View File
@@ -58,7 +58,7 @@ type
class function Assign(const AIdentifier: IIdentifierNode; const AValue: IAstNode): IAssignmentNode; static;
class function AssignResult(const AValue: IAstNode): IAssignmentNode; static; deprecated;
class function Indexer(const ABase: IAstNode; const AIndex: IAstNode): IIndexerNode; static;
class function MemberAccess(const ABase: IAstNode; const AMember: IIdentifierNode): IMemberAccessNode; static;
class function MemberAccess(const ABase: IAstNode; const AMember: IKeywordNode): IMemberAccessNode; static;
class function RecordLiteral(const AFields: TArray<TRecordFieldLiteral>): IRecordLiteralNode; static;
class function CreateSeries(const ADefinition: String): ICreateSeriesNode; static;
class function AddSeriesItem(
@@ -104,7 +104,7 @@ type
FValue: IKeyword;
function GetValue: IKeyword;
public
constructor Create(const AName: string);
constructor Create(const AValue: IKeyword);
function Accept(const Visitor: IAstVisitor): TDataValue; override;
property Value: IKeyword read FValue;
end;
@@ -292,11 +292,11 @@ type
TMemberAccessNode = class(TAstNode, IMemberAccessNode)
private
FBase: IAstNode;
FMember: IIdentifierNode;
FMember: IKeywordNode;
function GetBase: IAstNode;
function GetMember: IIdentifierNode;
function GetMember: IKeywordNode;
public
constructor Create(const ABase: IAstNode; const AMember: IIdentifierNode);
constructor Create(const ABase: IAstNode; const AMember: IKeywordNode);
function Accept(const Visitor: IAstVisitor): TDataValue; override;
end;
@@ -349,14 +349,15 @@ constructor TConstantNode.Create(const AValue: TDataValue);
begin
inherited Create;
// Validate that only allowed constant kinds are used.
if not (AValue.Kind in [vkScalar, vkText, vkVoid]) then
raise EArgumentException.Create('IConstantNode only supports Scalar, Text, and Void values.');
if not (AValue.Kind in [vkScalar, vkText, vkVoid, vkKeyword]) then
raise EArgumentException.Create('IConstantNode only supports Scalar, Text, Void and Keyword values.');
FValue := AValue;
case AValue.Kind of
vkScalar: StaticType := TTypes.FromScalarKind(AValue.AsScalar.Kind);
vkText: StaticType := TTypes.Text;
vkVoid: StaticType := TTypes.Void;
vkKeyword: StaticType := TTypes.Keyword;
else
StaticType := TTypes.Unknown; // Should not happen due to validation above
end;
@@ -392,11 +393,12 @@ end;
{ TKeywordNode }
constructor TKeywordNode.Create(const AName: string);
constructor TKeywordNode.Create(const AValue: IKeyword);
begin
inherited Create;
FValue := TKeywordRegistry.Intern(AName);
FValue := AValue;
StaticType := TTypes.Keyword;
FValue := AValue;
end;
function TKeywordNode.Accept(const Visitor: IAstVisitor): TDataValue;
@@ -788,7 +790,7 @@ end;
{ TMemberAccessNode }
constructor TMemberAccessNode.Create(const ABase: IAstNode; const AMember: IIdentifierNode);
constructor TMemberAccessNode.Create(const ABase: IAstNode; const AMember: IKeywordNode);
begin
inherited Create;
FBase := ABase;
@@ -805,7 +807,7 @@ begin
Result := FBase;
end;
function TMemberAccessNode.GetMember: IIdentifierNode;
function TMemberAccessNode.GetMember: IKeywordNode;
begin
Result := FMember;
end;
@@ -992,7 +994,7 @@ end;
class function TAst.Keyword(const AName: string): IKeywordNode;
begin
Result := TKeywordNode.Create(AName);
Result := TKeywordNode.Create(TKeywordRegistry.Intern(AName));
end;
class function TAst.LambdaExpr(const AParameters: TArray<IIdentifierNode>; const ABody: IAstNode): ILambdaExpressionNode;
@@ -1009,7 +1011,7 @@ begin
Result := TMacroDefinitionNode.Create(AName, AParameters, ABody);
end;
class function TAst.MemberAccess(const ABase: IAstNode; const AMember: IIdentifierNode): IMemberAccessNode;
class function TAst.MemberAccess(const ABase: IAstNode; const AMember: IKeywordNode): IMemberAccessNode;
begin
Result := TMemberAccessNode.Create(ABase, AMember);
end;