Ast Binding

This commit is contained in:
Michael Schimmel
2025-09-04 01:41:09 +02:00
parent de052cab64
commit 9c90a92b04
6 changed files with 394 additions and 382 deletions
+78 -30
View File
@@ -45,7 +45,7 @@ type
// --- Static method to run the binder ---
// Traverses the AST and resolves all identifiers. This must be called before evaluating the AST.
class procedure Bind(const ARootNode: IExpressionNode); static;
class procedure Bind(const ARootNode: IExpressionNode; const AScope: IExecutionScope); static;
end;
// TAstTraverser provides a default AST traversal implementation.
@@ -73,7 +73,8 @@ implementation
uses
System.Classes,
System.Generics.Collections;
System.Generics.Collections,
Myc.Ast.Scope; // Needed for the TExecutionScope cast
type
{ TAstNode }
@@ -296,9 +297,11 @@ type
FParent: TSymbolTable;
FSymbols: TDictionary<string, TSymbol>;
FNextSlotIndex: Integer;
procedure DefinePreResolved(const Name: string; Index: Integer);
public
constructor Create(AParent: TSymbolTable);
destructor Destroy; override;
procedure PopulateFromScope(const AScope: IExecutionScope);
function Define(const Name: string): TSymbol;
function Resolve(const Name: string; out Symbol: TSymbol; out Depth: Integer): Boolean;
end;
@@ -310,12 +313,11 @@ type
procedure EnterScope;
procedure ExitScope;
public
constructor Create;
constructor Create(AInitialScope: IExecutionScope);
destructor Destroy; override;
function VisitIdentifier(const Node: IIdentifierNode): TAstValue; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TAstValue; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue; override;
end;
{ TConstantNode }
@@ -745,6 +747,38 @@ begin
inherited;
end;
procedure TSymbolTable.DefinePreResolved(const Name: string; Index: Integer);
var
symbol: TSymbol;
begin
// Defines a symbol with a specific, pre-determined index.
if not FSymbols.ContainsKey(Name) then
begin
symbol.Name := Name;
symbol.SlotIndex := Index;
FSymbols.Add(Name, symbol);
// Ensure the next automatically assigned index is correct.
if (Index >= FNextSlotIndex) then
FNextSlotIndex := Index + 1;
end;
end;
procedure TSymbolTable.PopulateFromScope(const AScope: IExecutionScope);
var
scopeImpl: TExecutionScope;
pair: TPair<string, Integer>;
begin
// ** THE FIX IS HERE **
// Iterate over the Key-Value pairs of the scope's dictionary to preserve the exact indices.
if not Assigned(AScope) then
Exit;
scopeImpl := (AScope as TExecutionScope);
for pair in scopeImpl.NameToIndex do
DefinePreResolved(pair.Key, pair.Value);
end;
function TSymbolTable.Define(const Name: string): TSymbol;
begin
Result.Name := Name;
@@ -772,16 +806,41 @@ end;
{ TBinder }
constructor TBinder.Create;
// Helper function to recursively build the symbol table hierarchy
function CreateSymbolTableFromScope(AScope: IExecutionScope): TSymbolTable;
begin
inherited;
FCurrentScope := TSymbolTable.Create(nil); // Start with a global scope
if not Assigned(AScope) then
Exit(nil);
// Recursively create the parent symbol table first
Result := TSymbolTable.Create(CreateSymbolTableFromScope(AScope.Parent));
// Then populate the current level
Result.PopulateFromScope(AScope);
end;
constructor TBinder.Create(AInitialScope: IExecutionScope);
begin
inherited Create;
// Correctly and recursively build the symbol table hierarchy from the execution scope.
FCurrentScope := CreateSymbolTableFromScope(AInitialScope);
// If no initial scope was provided, create a single empty root scope.
if not Assigned(FCurrentScope) then
FCurrentScope := TSymbolTable.Create(nil);
end;
destructor TBinder.Destroy;
var
scopeToFree: TSymbolTable;
begin
Assert(not Assigned(FCurrentScope.FParent), 'Scope leak in binder.');
FCurrentScope.Free;
// The previous Assert was incorrect for binders initialized with nested scopes.
// This new implementation robustly cleans up the entire symbol table chain.
while Assigned(FCurrentScope) do
begin
scopeToFree := FCurrentScope;
FCurrentScope := FCurrentScope.FParent;
scopeToFree.Free;
end;
inherited;
end;
@@ -799,17 +858,6 @@ begin
oldScope.Free;
end;
function TBinder.VisitBlockExpression(const Node: IBlockExpressionNode): TAstValue;
begin
EnterScope;
try
// Default traversal of all expressions in the block
inherited VisitBlockExpression(Node);
finally
ExitScope;
end;
end;
function TBinder.VisitIdentifier(const Node: IIdentifierNode): TAstValue;
var
symbol: TSymbol;
@@ -836,6 +884,9 @@ var
begin
EnterScope;
try
// Reserve a slot for 'Self' first.
FCurrentScope.Define('Self');
// 1. Define all parameters in the new scope.
for param in Node.Parameters do
FCurrentScope.Define(param.Name);
@@ -850,24 +901,21 @@ end;
function TBinder.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TAstValue;
begin
// 1. First, visit the initializer expression to resolve its identifiers.
if Assigned(Node.Initializer) then
Node.Initializer.Accept(Self);
// 2. Then, define the new variable in the current scope.
// For recursive functions, we must define the name in the scope FIRST,
// so it can be resolved inside its own initializer (i.e., the lambda body).
FCurrentScope.Define(Node.Identifier.Name);
// 3. Finally, visit the declaration's own identifier to resolve it.
Node.Identifier.Accept(Self);
inherited;
end;
{ TAst }
class procedure TAst.Bind(const ARootNode: IExpressionNode);
class procedure TAst.Bind(const ARootNode: IExpressionNode; const AScope: IExecutionScope);
var
binder: IAstVisitor;
begin
binder := TBinder.Create;
// Create a binder instance, pre-populating its symbol table with the given scope.
binder := TBinder.Create(AScope);
// Traverse the AST to resolve all identifiers.
ARootNode.Accept(binder);
end;