This commit is contained in:
Michael Schimmel
2026-01-04 18:48:04 +01:00
parent a4afae6f39
commit 991b998cb1
17 changed files with 534 additions and 102 deletions
+135 -48
View File
@@ -70,6 +70,7 @@ type
function VisitRecordLiteral(const Node: IAstNode): IAstNode;
function VisitConstant(const Node: IAstNode): IAstNode;
function VisitKeyword(const Node: IAstNode): IAstNode;
function VisitTuple(const Node: IAstNode): IAstNode;
// Pipe Support
function VisitPipeInput(const Node: IAstNode): IAstNode;
@@ -238,6 +239,7 @@ begin
Register(akRecordLiteral, VisitRecordLiteral);
Register(akConstant, VisitConstant);
Register(akKeyword, VisitKeyword);
Register(akTuple, VisitTuple);
// Pipe Support
Register(akPipeInput, VisitPipeInput);
@@ -292,7 +294,6 @@ end;
function TTypeChecker.VisitConstant(const Node: IAstNode): IAstNode;
begin
// Base implementation already returns Node, but here we explicitly confirm identity for clarity
Result := Node;
end;
@@ -301,6 +302,98 @@ begin
Result := Node;
end;
function TTypeChecker.VisitTuple(const Node: IAstNode): IAstNode;
var
T: ITupleNode;
newElements: TArray<IAstNode>;
elementTypes: TArray<IStaticType>;
i: Integer;
// Inference variables
firstType: IStaticType;
isHomogeneous: Boolean;
commonDim: TArray<Integer>;
newDim: TArray<Integer>;
finalType: IStaticType;
begin
T := Node.AsTuple;
var count := Length(T.Elements);
SetLength(newElements, count);
SetLength(elementTypes, count);
// 1. Visit Children
// Recursively type-check all elements first to determine their static types.
for i := 0 to count - 1 do
begin
newElements[i] := Accept(T.Elements[i]);
elementTypes[i] := newElements[i].AsTypedNode.StaticType;
end;
// 2. Inference Logic: Tuple vs. Vector vs. Matrix
if count = 0 then
begin
// Empty Tuple -> stTuple (safest fallback, effectively Void)
finalType := TTypes.CreateTuple([]);
end
else
begin
firstType := elementTypes[0];
isHomogeneous := True;
// Check for Homogeneity (Exact type equality of all elements)
for i := 1 to count - 1 do
begin
if not firstType.IsEqual(elementTypes[i]) then
begin
isHomogeneous := False;
break;
end;
end;
if isHomogeneous then
begin
// It is at least a Vector.
// Check if it should be promoted to a Matrix (i.e., elements are Vectors or Matrices).
if firstType.Kind = stVector then
begin
// Vector of Vectors -> Matrix (2D)
// New Dimensions = [OuterCount, InnerCount]
newDim := [count, firstType.AsVector.Count];
finalType := TTypes.CreateMatrix(firstType.AsVector.ElementType, newDim);
end
else if firstType.Kind = stMatrix then
begin
// Vector of Matrices -> Higher dimensional Matrix (N+1)
// New Dimensions = [OuterCount, Dim0, Dim1...]
commonDim := firstType.AsMatrix.Dimensions;
SetLength(newDim, Length(commonDim) + 1);
newDim[0] := count;
for i := 0 to High(commonDim) do
newDim[i + 1] := commonDim[i];
finalType := TTypes.CreateMatrix(firstType.AsMatrix.ElementType, newDim);
end
else
begin
// Base case: Homogeneous Scalars/Records -> Vector (1D)
finalType := TTypes.CreateVector(firstType, count);
end;
end
else
begin
// Heterogeneous types -> Standard Tuple
finalType := TTypes.CreateTuple(elementTypes);
end;
end;
// 3. Return the new node with the inferred type definition
Result := TAst.Tuple(Node.Identity, newElements, finalType);
end;
function TTypeChecker.VisitIdentifier(const Node: IAstNode): IAstNode;
var
I: IIdentifierNode;
@@ -328,12 +421,6 @@ var
args: IArgumentList;
begin
R := Node.AsRecur;
// Use inherited to transform arguments, then reconstruct with type Void
// Note: inherited VisitRecurNode returns IAstNode which is a RecurNode.
// We can call Accept on arguments list directly to avoid intermediate node creation if desired,
// but relying on inherited logic keeps it consistent.
// Efficient approach: Accept the arguments list directly (it's a child).
args := Accept(R.Arguments).AsArgumentList;
Result := TAst.Recur(Node.Identity, args, TTypes.Void);
end;
@@ -422,17 +509,37 @@ var
newBlock: IBlockExpressionNode;
blockType: IStaticType;
exprs: IExpressionList;
i: Integer;
begin
// Inherited logic transforms all expressions in the list
newBlock := inherited VisitBlockExpression(Node).AsBlockExpression;
// 1. Transform children via inherited logic
var transformedNode := inherited VisitBlockExpression(Node);
// Safety check: Did the transformer return a node?
if not Assigned(transformedNode) then
raise ECompilationFailed.Create([TCompilerError.Create(elError, 'Internal Error: Block transformation returned nil.', Node)]);
newBlock := transformedNode.AsBlockExpression;
exprs := newBlock.Expressions;
// 2. Validate expressions
if exprs.Count > 0 then
blockType := exprs[exprs.Count - 1].AsTypedNode.StaticType
else
blockType := TTypes.Void;
begin
// Check for NIL entries which indicate failed transformation of children
for i := 0 to exprs.Count - 1 do
begin
if exprs[i] = nil then
raise ECompilationFailed.Create(
[TCompilerError.Create(elError, Format('Internal Error: Block expression #%d transformed to nil.', [i]), Node)]);
end;
// The type of the block is the type of the last expression
blockType := exprs[exprs.Count - 1].AsTypedNode.StaticType;
end
else
begin
blockType := TTypes.Void;
end;
// Return new block with calculated type
Result := TAst.Block(Node.Identity, exprs, blockType);
end;
@@ -451,7 +558,6 @@ var
begin
L := Node.AsLambdaExpression;
// 1. Resolve Upvalue Types
var upvalueAddrs := L.Upvalues;
SetLength(upvalueTypes, Length(upvalueAddrs));
@@ -466,7 +572,6 @@ begin
upvalueTypes[i] := FCurrentContext.LookupType(lookupAddr);
end;
// 2. Enter New Scope
FCurrentContext := TTypeContext.Create(FCurrentContext, L.Layout, upvalueTypes, nil);
try
SetLength(newParams, L.Parameters.Count);
@@ -475,8 +580,6 @@ begin
for i := 0 to L.Parameters.Count - 1 do
begin
paramIdent := L.Parameters[i];
// Check if there is already a type assigned (e.g. injected by Pipe Visitor)
injectedType := paramIdent.AsTypedNode.StaticType;
if injectedType.Kind = stUnknown then
@@ -517,10 +620,7 @@ var
bestSig: IMethodSignature;
match: Boolean;
begin
// Use inherited to visit Callee and Arguments first
newCall := inherited VisitFunctionCall(Node).AsFunctionCall;
// Now analyze types on the transformed children
var newCallee := newCall.Callee;
var newArgs := newCall.Arguments;
@@ -570,7 +670,6 @@ begin
if Assigned(FLog) then
FLog.AddError(Format('Cannot invoke type %s as a function.', [calleeType.ToString]), Node);
// Return new node with Types
Result := TAst.FunctionCall(Node.Identity, newCallee, newArgs, retType, newCall.IsTailCall, nil, False);
end;
@@ -605,7 +704,18 @@ begin
if baseType.Kind = stSeries then
elemType := baseType.AsSeries.ElementType
else if baseType.Kind = stRecordSeries then
elemType := TTypes.CreateRecord(baseType.AsRecord.Definition);
elemType := TTypes.CreateRecord(baseType.AsRecord.Definition)
else if baseType.Kind = stTuple then
begin
// NEW: Tuple Indexing (requires constant index for strong typing!)
if (newIndex.Kind = akConstant) and (newIndex.AsConstant.Value.Kind = vkScalar) then
begin
var idx := newIndex.AsConstant.Value.AsScalar.Value.AsInt64;
var tpl := baseType.AsTuple;
if (idx >= 0) and (idx < tpl.Count) then
elemType := tpl.Elements[Integer(idx)];
end;
end;
elemType := ApplyOptionality(elemType, isOpt);
Result := TAst.Indexer(Node.Identity, newBase, newIndex, elemType);
@@ -655,10 +765,8 @@ var
newFieldList: IRecordFieldList;
begin
R := Node.AsRecordLiteral;
// Transform fields using inherited recursion
newFieldList := inherited VisitRecordFieldList(R.Fields).AsRecordFieldList;
// Now analyze the transformed fields
var count := newFieldList.Count;
SetLength(fieldTypes, count);
SetLength(scalarFieldTypes, count);
@@ -670,7 +778,6 @@ begin
key := field.Key.Value;
valType := field.Value.AsTypedNode.StaticType;
// Check if strict scalar (no optionals allowed in packed ScalarRecord)
if (valType.Kind in [stOrdinal, stFloat, stBoolean, stDateTime, stKeyword]) and (not valType.IsOptional) then
begin
var kind: TScalar.TKind;
@@ -693,7 +800,6 @@ begin
fieldTypes[i] := TPair<IKeyword, IStaticType>.Create(key, valType);
end;
// Build Definition
var scalarDef: IScalarRecordDefinition := nil;
var genericDef: IGenericRecordDefinition := nil;
var resultType: IStaticType;
@@ -720,9 +826,8 @@ var
begin
C := Node.AsCreateSeries;
def := C.Definition;
// Simple heuristic for type
if def.StartsWith('[') then
elemType := TTypes.CreateRecord(nil) // Placeholder, normally parses JSON
elemType := TTypes.CreateRecord(nil)
else
elemType := TTypes.FromScalarKind(TScalar.StringToKind(def));
@@ -750,11 +855,9 @@ var
sourceType: IStaticType;
begin
P := Node.AsPipeInput;
// Transform source identifier (resolves type)
newSource := Accept(P.StreamSource).AsIdentifier;
sourceType := newSource.AsTypedNode.StaticType;
// 1. Verify Source is a Series-compatible type
if (sourceType.Kind <> stUnknown) then
begin
if not ((sourceType.Kind = stSeries) or (sourceType.Kind = stRecordSeries)) then
@@ -767,7 +870,6 @@ begin
end
else if (sourceType.Kind = stRecordSeries) then
begin
// 2. Verify Selectors exist in Record Definition
var def := sourceType.AsRecord.Definition;
for var sel in P.Selectors do
begin
@@ -780,7 +882,6 @@ begin
end;
end;
// Reuse selectors (they are just keywords, no type checking needed)
Result := TAst.PipeInput(newSource, P.Selectors, Node.Identity.Location);
end;
@@ -800,22 +901,18 @@ begin
SetLength(newInputs, P.Inputs.Count);
paramTypes := TList<IStaticType>.Create;
try
// 1. Visit Inputs and collect types for Lambda parameters
for i := 0 to P.Inputs.Count - 1 do
begin
// Recurse on inputs to resolve their sources
inputNode := Accept(P.Inputs[i]).AsPipeInput;
newInputs[i] := inputNode;
streamType := inputNode.StreamSource.AsTypedNode.StaticType;
// Flatten logic: One lambda param per selector
for var sel in inputNode.Selectors do
begin
inferredType := TTypes.Unknown;
if streamType.Kind = stRecordSeries then
begin
// Extract field type from definition
var def := streamType.AsRecord.Definition;
var idx := def.IndexOf(sel.Value);
if idx >= 0 then
@@ -823,18 +920,16 @@ begin
end
else if streamType.Kind = stSeries then
begin
// If simple series, use its element type
if Assigned(streamType.AsSeries.ElementType) then
inferredType := streamType.AsSeries.ElementType
else
inferredType := TTypes.Ordinal; // Fallback default
inferredType := TTypes.Ordinal;
end;
paramTypes.Add(inferredType);
end;
end;
// 2. Prepare Lambda with Inferred Types
lambda := P.Transformation;
if lambda.Parameters.Count <> paramTypes.Count then
@@ -857,11 +952,9 @@ begin
if k < paramTypes.Count then paramTypes[k]
else TTypes.Unknown;
// Create new identifier with the inferred type!
newParams[k] := TAst.Identifier(oldP.Identity.AsNamed, oldP.Address, typeToInject);
end;
// Recreate Lambda NODE with Typed Parameters
var preTypedLambda :=
TAst.LambdaExpr(
lambda.Identity,
@@ -872,24 +965,20 @@ begin
lambda.Upvalues,
lambda.HasNestedLambdas,
lambda.IsPure,
TTypes.Unknown // Will be recalculated in Accept
TTypes.Unknown
);
// 3. Visit the Lambda (This will now type-check the Body using the types we just injected)
var typedLambda := Accept(preTypedLambda).AsLambdaExpression;
// 4. Infer Pipe Return Type & Validate Strictness
var lambdaRetType := typedLambda.AsTypedNode.StaticType.AsMethod.Signatures[0].ReturnType;
var pipeType: IStaticType;
if lambdaRetType.Kind = stRecord then
begin
// Valid: Record -> RecordSeries
pipeType := TTypes.CreateRecordSeries(lambdaRetType.AsRecord.Definition);
end
else
begin
// STRICT CHECK: Scalars are NOT allowed.
if (lambdaRetType.Kind <> stUnknown) and (lambdaRetType.Kind <> stVoid) then
begin
if Assigned(FLog) then
@@ -898,8 +987,6 @@ begin
lambda
);
end;
// If Void or Unknown, or Error case:
pipeType := TTypes.Unknown;
end;