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);