Keywords as basic scalar type

This commit is contained in:
Michael Schimmel
2025-10-31 21:15:08 +01:00
parent 8abec8e98f
commit 689dede600
9 changed files with 292 additions and 264 deletions
+16 -22
View File
@@ -259,14 +259,10 @@ begin
// externally evaluate the expression using the injected evaluator
value := FEvaluate(expr);
// Allow unquoting keywords
if value.Kind in [vkScalar, vkText, vkVoid, vkKeyword] then
// Allow unquoting scalars (which now include keywords)
if value.Kind in [vkScalar, vkText, vkVoid] then
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));
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]);
@@ -795,7 +791,6 @@ begin
Result := SetType(TDataValue.FromIntf<IConstantNode>(Node), TTypes.FromScalarKind(Node.Value.AsScalar.Kind));
TDataValueKind.vkText: Result := SetType(TDataValue.FromIntf<IConstantNode>(Node), TTypes.Text);
TDataValueKind.vkVoid: Result := SetType(TDataValue.FromIntf<IConstantNode>(Node), TTypes.Void);
TDataValueKind.vkKeyword: Result := SetType(TDataValue.FromIntf<IConstantNode>(Node), TTypes.Keyword);
else
// Handle other constant types if they become supported
Result := SetType(TDataValue.FromIntf<IConstantNode>(Node), TTypes.Unknown);
@@ -805,7 +800,8 @@ end;
function TAstBinder.VisitKeyword(const Node: IKeywordNode): TDataValue;
begin
// Keywords are literals. Their type is set in TKeywordNode.Create.
Result := TDataValue.FromIntf<IKeywordNode>(Node);
// We also set the static type on the node itself during binding.
Result := SetType(TDataValue.FromIntf<IKeywordNode>(Node), TTypes.Keyword);
end;
function TAstBinder.VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
@@ -966,34 +962,32 @@ var
boundNode: IRecordLiteralNode;
valNode: IAstNode;
valType: IStaticType;
scalarKind: TScalar.TKind;
begin
FNextIsTail := False;
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;
// Path A: Records can only store scalar values
if not (valType.Kind in [stOrdinal, stFloat]) then
// Path A: Records can now store Ordinal, Float, or Keyword
if (valType.Kind = stOrdinal) then
scalarKind := TScalar.TKind.Ordinal
else if (valType.Kind = stFloat) then
scalarKind := TScalar.TKind.Float
else if (valType.Kind = stKeyword) then
scalarKind := TScalar.TKind.Keyword
else
raise ETypeException.CreateFmt(
'Record fields must be scalar (Ordinal or Float), but field ":%s" is %s',
'Record fields must be scalar (Ordinal, Float, or Keyword), but field ":%s" is %s',
[Node.Fields[i].Key.Value.Name, valType.ToString]);
boundFields[i] := TRecordFieldLiteral.Create(Node.Fields[i].Key, valNode);
var scalarKind: TScalar.TKind;
if valType.Kind = stOrdinal then
scalarKind := TScalar.TKind.Ordinal
else
scalarKind := TScalar.TKind.Float;
// Create the definition field using the Keyword's name
// Create the definition field using the Keyword and its TScalar.TKind
defFields[i] := TScalarRecordField.Create(Node.Fields[i].Key.Value, scalarKind);
end;
+39 -41
View File
@@ -152,17 +152,16 @@ end;
function TEvaluatorVisitor.IsTruthy(const AValue: TDataValue): Boolean;
begin
// Other types (Text, Series, etc.) are considered "false" in a boolean context
if (AValue.Kind <> vkScalar) then
begin
Result := False;
exit;
end;
exit(false);
case AValue.AsScalar.Kind of
TScalar.TKind.Ordinal: Result := (AValue.AsScalar.Value.AsInt64 <> 0);
TScalar.TKind.Float: Result := (AValue.AsScalar.Value.AsDouble <> 0.0);
TScalar.TKind.Ordinal: Result := AValue.AsScalar.Value.AsInt64 <> 0;
TScalar.TKind.Float: Result := AValue.AsScalar.Value.AsDouble <> 0.0;
TScalar.TKind.Keyword: Result := AValue.AsScalar.Value.AsInt64 <> 0;
else
Result := False;
Result := false;
end;
end;
@@ -368,8 +367,8 @@ end;
function TEvaluatorVisitor.VisitKeyword(const Node: IKeywordNode): TDataValue;
begin
// Return the shared, interned IKeyword interface as a TDataValue
Result := TDataValue.FromKeyword(Node.Value);
// Return the keyword as a TScalar value
Result := TDataValue(TScalar.FromKeyword(Node.Value));
end;
function TEvaluatorVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
@@ -402,6 +401,8 @@ function TEvaluatorVisitor.VisitIndexer(const Node: IIndexerNode): TDataValue;
var
baseValue, indexValue: TDataValue;
index: Int64;
series: ISeries;
recSeries: IRecordSeries;
begin
baseValue := Node.Base.Accept(Self);
indexValue := Node.Index.Accept(Self);
@@ -414,12 +415,23 @@ begin
case baseValue.Kind of
vkSeries:
begin
with baseValue.AsSeries do
begin
if (index < 0) or (index >= TotalCount) then
raise EArgumentException.CreateFmt('Index %d is out of bounds for series with %d elements.', [index, TotalCount]);
Result := Items[Integer(index)];
end;
series := baseValue.AsSeries;
if (index < 0) or (index >= series.TotalCount) then
raise EArgumentException.CreateFmt('Index %d is out of bounds for series with %d elements.', [index, series.TotalCount]);
Result := series.Items[Integer(index)];
end;
vkRecordSeries:
begin
recSeries := baseValue.AsRecordSeries;
if (index < 0) or (index >= recSeries.TotalCount) then
raise EArgumentException.CreateFmt('Index %d is out of bounds for series with %d elements.', [index, recSeries.TotalCount]);
// Accessing a record series by index materializes the TScalarRecord
var rec := TScalarRecord.Create(recSeries.Def, nil); // Nil fields, needs implementation
// TODO: This path (materializing a record from TScalarRecordSeries) is not fully implemented.
// We need to fetch the underlying TScalar.TValue array slice.
// For now, returning an empty record shell.
raise ENotSupportedException.Create('Indexing a RecordSeries to materialize a Record is not yet implemented.');
Result := TDataValue.FromRecord(rec);
end;
else
raise EArgumentException.Create('Indexer `[]` is not supported for this value type.');
@@ -455,8 +467,17 @@ begin
begin
// Evaluate the value expression for this field
valData := boundNode.Fields[i].Value.Accept(Self);
// The binder already validated this is a scalar
values[i] := valData.AsScalar.Value;
if valData.Kind = vkScalar then
begin
// Binder ensures it's a valid scalar kind (Ordinal, Float, Keyword)
values[i] := valData.AsScalar.Value;
end
else
begin
// This should be unreachable if binder worked correctly
raise EInvalidOpException.Create('Invalid type found in record literal during evaluation.');
end;
end;
// Create the TScalarRecord using the definition from the binder
@@ -501,32 +522,9 @@ begin
leftValue := Node.Left.Accept(Self);
rightValue := Node.Right.Accept(Self);
// Handle Keyword equality
if (leftValue.Kind = vkKeyword) or (rightValue.Kind = vkKeyword) then
begin
if not (leftValue.Kind = rightValue.Kind) then
begin
// Comparing keyword to non-keyword
if Node.Operator = TScalar.TBinaryOp.NotEqual then
Result := TScalar.FromInt64(1)
else
Result := TScalar.FromInt64(0);
exit;
end;
// Both are keywords, compare interfaces
case Node.Operator of
TScalar.TBinaryOp.Equal: Result := TScalar.FromInt64(Ord(leftValue.AsKeyword = rightValue.AsKeyword));
TScalar.TBinaryOp.NotEqual: Result := TScalar.FromInt64(Ord(leftValue.AsKeyword <> rightValue.AsKeyword));
else
raise ENotSupportedException.Create('Only = and <> operations are supported for Keywords.');
end;
exit;
end;
// Standard scalar operations
if (leftValue.Kind <> vkScalar) or (rightValue.Kind <> vkScalar) then
raise ENotSupportedException.Create('Binary operations are only supported for scalar or keyword types.');
raise ENotSupportedException.Create('Binary operations are only supported for scalar types.');
if not TScalar.TryBinaryOperation(Node.Operator, leftValue.AsScalar, rightValue.AsScalar, resScalar) then
raise ENotSupportedException.Create(
+10 -5
View File
@@ -114,6 +114,7 @@ end;
procedure TJsonAstConverter.DataValueToJson(const AValue: TDataValue; const AParent: TJSONObject; const AName: string);
var
valObj, scalarObj: TJSONObject;
kwName: string;
begin
valObj := TJSONObject.Create;
valObj.AddPair('Kind', TJSONString.Create(AValue.Kind.ToString));
@@ -126,11 +127,16 @@ begin
case AValue.AsScalar.Kind of
TScalar.TKind.Ordinal: scalarObj.AddPair('Value', TJSONNumber.Create(AValue.AsScalar.Value.AsInt64));
TScalar.TKind.Float: scalarObj.AddPair('Value', TJSONNumber.Create(AValue.AsScalar.Value.AsDouble));
TScalar.TKind.Keyword:
begin
// Use reverse map to get name from index (AsInt64)
kwName := TKeywordRegistry.GetName(AValue.AsScalar.Value.AsInt64);
scalarObj.AddPair('Value', TJSONString.Create(kwName));
end;
end;
valObj.AddPair('Value', scalarObj);
end;
vkText: valObj.AddPair('Value', TJSONString.Create(AValue.AsText));
vkKeyword: valObj.AddPair('Value', TJSONString.Create(AValue.AsKeyword.Name));
vkVoid:; // No value to add
else
raise ENotSupportedException.Create('Unsupported TDataValue kind for constant serialization.');
@@ -660,16 +666,15 @@ begin
case scalarKind of
TScalar.TKind.Ordinal: Result := TScalar.FromInt64(scalarObj.GetValue<TJSONNumber>('Value').AsInt64);
TScalar.TKind.Float: Result := TScalar.FromDouble(scalarObj.GetValue<TJSONNumber>('Value').AsDouble);
TScalar.TKind.Keyword:
// Keywords are serialized by name, intern them back
Result := TScalar.FromKeyword(TKeywordRegistry.Intern(scalarObj.GetValue<string>('Value')));
end;
end
else if SameText(kindStr, 'Text') then
begin
Result := valObj.GetValue<string>('Value');
end
else if SameText(kindStr, 'Keyword') then
begin
Result := TDataValue.FromKeyword(TKeywordRegistry.Intern(valObj.GetValue<string>('Value')));
end
else if SameText(kindStr, 'Void') then
begin
Result := TDataValue.Void;
+22 -16
View File
@@ -381,6 +381,9 @@ begin
exit(False);
var otherDef := Other.Definition;
if (not Assigned(Self.FDefinition)) or (not Assigned(otherDef)) then
exit(False); // Should not happen if Kind is equal, but defensive check
if Length(Self.FDefinition.Fields) <> Length(otherDef.Fields) then
exit(False);
@@ -399,11 +402,14 @@ var
i: Integer;
begin
Result := GetKind.ToString + '{';
for i := 0 to High(FDefinition.Fields) do
if Assigned(FDefinition) then
begin
Result := Result + FDefinition.Fields[i].Key.Name + ': ' + FDefinition.Fields[i].Value.ToString;
if i < High(FDefinition.Fields) then
Result := Result + ', ';
for i := 0 to High(FDefinition.Fields) do
begin
Result := Result + FDefinition.Fields[i].Key.Name + ': ' + FDefinition.Fields[i].Value.ToString;
if i < High(FDefinition.Fields) then
Result := Result + ', ';
end;
end;
Result := Result + '}';
end;
@@ -446,6 +452,7 @@ begin
case AKind of
TScalar.TKind.Ordinal: Result := FOrdinal;
TScalar.TKind.Float: Result := FFloat;
TScalar.TKind.Keyword: Result := FKeyword;
else
raise ETypeException.Create('Cannot convert invalid TScalar.TKind to TStaticType.');
end;
@@ -472,6 +479,9 @@ begin
if (Target.Kind = stVoid) and (Source.Kind = stMethod) then
exit(True);
if (Target.Kind = stOrdinal) and (Source.Kind = stKeyword) then
exit(True);
// TODO: Implement full assignment compatibility rules (e.g., for records/series)
Result := False;
@@ -499,11 +509,11 @@ begin
if (A.Kind = stOrdinal) and (B.Kind = stOrdinal) then
exit(TTypes.Ordinal);
// If types are identical and not numeric, return that type (e.g. Text + Text might be valid later)
// If types are identical (incl. Keyword, Text, etc.), return that type.
if A.IsEqual(B) then
exit(A);
// Cannot promote other combinations during basic inference
// Cannot promote other combinations (e.g., Ordinal and Keyword)
raise ETypeException.CreateFmt('Cannot promote types %s and %s', [A.ToString, B.ToString]);
end;
@@ -551,24 +561,18 @@ begin
TScalar.TBinaryOp.Equal, TScalar.TBinaryOp.NotEqual:
begin
// Allow equality checks for Keywords
if (promotedKind = stKeyword) then
begin
Result := TTypes.Ordinal;
exit;
end;
// Comparison requires Ordinal or Float, but *always* results in Ordinal (boolean)
if not (promotedKind in [stOrdinal, stFloat]) then
// Allow equality checks for Ordinal, Float, or Keyword
if not (promotedKind in [stOrdinal, stFloat, stKeyword]) then
raise ETypeException.CreateFmt(
'Comparison operator %s requires Ordinal, Float, or Keyword, but got %s after promotion',
[Op.ToString, promotedType.ToString]);
// Result is always Ordinal (boolean)
Result := TTypes.Ordinal;
end;
TScalar.TBinaryOp.Less, TScalar.TBinaryOp.Greater, TScalar.TBinaryOp.LessOrEqual, TScalar.TBinaryOp.GreaterOrEqual:
begin
// Comparison requires Ordinal or Float, but *always* results in Ordinal (boolean)
// Comparison requires Ordinal or Float (Keywords are not ordered)
if not (promotedKind in [stOrdinal, stFloat]) then
raise ETypeException.CreateFmt(
'Comparison operator %s requires Ordinal or Float, but got %s after promotion',
@@ -593,6 +597,7 @@ begin
case Op of
TScalar.TUnaryOp.Negate:
begin
// Negation requires Ordinal or Float
if not (rightKind in [stOrdinal, stFloat]) then
raise ETypeException.CreateFmt('Unary negation cannot be applied to %s', [Right.ToString]);
Result := Right; // Negation preserves type
@@ -600,6 +605,7 @@ begin
TScalar.TUnaryOp.Not:
begin
// Logical not only applies to Ordinal (booleans)
if not (rightKind = stOrdinal) then
raise ETypeException.CreateFmt('Logical not cannot be applied to %s', [Right.ToString]);
Result := TTypes.Ordinal;
+4 -4
View File
@@ -349,17 +349,17 @@ 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, vkKeyword]) then
raise EArgumentException.Create('IConstantNode only supports Scalar, Text, Void and Keyword values.');
// vkKeyword is no longer a valid TDataValue kind.
if not (AValue.Kind in [vkScalar, vkText, vkVoid]) then
raise EArgumentException.Create('IConstantNode only supports Scalar, Text, and Void 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
StaticType := TTypes.Unknown; // Should not happen
end;
end;