Add symbol table with scope nesting and built-in types

TSymbolTable manages a scope stack with case-insensitive lookup. Each
TScope holds a sorted TStringList (names → TSymbol) and chains to its
parent for identifier resolution. Built-in primitives (Integer, Int64,
UInt32, Byte, Boolean, string) and I/O procedures (Write, WriteLn) are
pre-registered in the global scope. 29 FPCUnit tests, all passing.
This commit is contained in:
Graeme Geldenhuys 2026-04-20 17:52:09 +01:00
parent 95e1ea58af
commit a561525ebd
3 changed files with 869 additions and 1 deletions

View file

@ -0,0 +1,335 @@
unit uSymbolTable;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, contnrs;
type
{ ------------------------------------------------------------------ }
{ Type descriptors }
{ ------------------------------------------------------------------ }
TTypeKind = (
tyInteger, { Int32 — QBE 'w' }
tyInt64, { Int64 — QBE 'l' }
tyUInt32, { UInt32 — QBE 'w' unsigned }
tyByte, { Byte — QBE 'b' }
tyBoolean, { Boolean — stored as byte, 0/1 only }
tyString, { ARC-managed UTF-8 string }
tyRecord, { Stack-allocated aggregate (Phase 2) }
tyClass, { Heap-allocated, single-inheritance (Phase 2) }
tyVoid { No value — used as procedure return type }
);
TTypeDesc = class
public
Kind: TTypeKind;
Name: string;
function IsNumeric: Boolean;
function IsString: Boolean;
function IsOrdinal: Boolean;
end;
{ ------------------------------------------------------------------ }
{ Symbols }
{ ------------------------------------------------------------------ }
TSymbolKind = (
skVariable,
skType,
skProcedure,
skFunction,
skParameter
);
TParamDesc = class
public
Name: string;
TypeDesc: TTypeDesc; { not owned }
IsConst: Boolean;
IsVar: Boolean;
end;
TSymbol = class
public
Name: string;
Kind: TSymbolKind;
TypeDesc: TTypeDesc; { not owned; nil for procedures }
Params: TObjectList; { owned TParamDesc; populated for procedures/functions }
constructor Create(const AName: string; AKind: TSymbolKind; AType: TTypeDesc);
destructor Destroy; override;
end;
{ ------------------------------------------------------------------ }
{ Scope }
{ ------------------------------------------------------------------ }
TScope = class
private
FParent: TScope;
FSymbols: TObjectList; { owns all TSymbol in this scope }
FKeys: TStringList; { sorted, case-insensitive; Objects[] = TSymbol (not owned) }
public
constructor Create(AParent: TScope);
destructor Destroy; override;
property Parent: TScope read FParent;
{ Returns False if name already defined in this scope; caller must free ASymbol. }
function Define(ASymbol: TSymbol): Boolean;
function LookupLocal(const AName: string): TSymbol;
function Lookup(const AName: string): TSymbol;
end;
{ ------------------------------------------------------------------ }
{ Symbol table }
{ ------------------------------------------------------------------ }
TSymbolTable = class
private
FScopeStack: TObjectList; { owned TScope, index 0 = global }
FAllTypes: TObjectList; { owned TTypeDesc }
FTypeInteger: TTypeDesc;
FTypeInt64: TTypeDesc;
FTypeUInt32: TTypeDesc;
FTypeByte: TTypeDesc;
FTypeBoolean: TTypeDesc;
FTypeString: TTypeDesc;
FTypeVoid: TTypeDesc;
function GetCurrentScope: TScope;
function GetScopeDepth: Integer;
function NewType(AKind: TTypeKind; const AName: string): TTypeDesc;
procedure RegisterBuiltins;
public
constructor Create;
destructor Destroy; override;
{ Scope management }
function PushScope: TScope;
procedure PopScope;
property CurrentScope: TScope read GetCurrentScope;
property ScopeDepth: Integer read GetScopeDepth;
{ Symbol management — owns ASymbol on success; caller must free on False }
function Define(ASymbol: TSymbol): Boolean;
function Lookup(const AName: string): TSymbol;
{ Type lookup — case-insensitive, returns nil if not found }
function FindType(const AName: string): TTypeDesc;
{ Convenience type accessors }
property TypeInteger: TTypeDesc read FTypeInteger;
property TypeInt64: TTypeDesc read FTypeInt64;
property TypeUInt32: TTypeDesc read FTypeUInt32;
property TypeByte: TTypeDesc read FTypeByte;
property TypeBoolean: TTypeDesc read FTypeBoolean;
property TypeString: TTypeDesc read FTypeString;
property TypeVoid: TTypeDesc read FTypeVoid;
end;
implementation
{ ------------------------------------------------------------------ }
{ TTypeDesc }
{ ------------------------------------------------------------------ }
function TTypeDesc.IsNumeric: Boolean;
begin
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyByte];
end;
function TTypeDesc.IsString: Boolean;
begin
Result := Kind = tyString;
end;
function TTypeDesc.IsOrdinal: Boolean;
begin
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyByte, tyBoolean];
end;
{ ------------------------------------------------------------------ }
{ TSymbol }
{ ------------------------------------------------------------------ }
constructor TSymbol.Create(const AName: string; AKind: TSymbolKind; AType: TTypeDesc);
begin
inherited Create;
Name := AName;
Kind := AKind;
TypeDesc := AType;
Params := TObjectList.Create(True);
end;
destructor TSymbol.Destroy;
begin
Params.Free;
inherited Destroy;
end;
{ ------------------------------------------------------------------ }
{ TScope }
{ ------------------------------------------------------------------ }
constructor TScope.Create(AParent: TScope);
begin
inherited Create;
FParent := AParent;
FSymbols := TObjectList.Create(True);
FKeys := TStringList.Create;
FKeys.Sorted := True;
FKeys.CaseSensitive := False;
FKeys.Duplicates := dupIgnore; { we check manually before inserting }
end;
destructor TScope.Destroy;
begin
FKeys.Free;
FSymbols.Free;
inherited Destroy;
end;
function TScope.Define(ASymbol: TSymbol): Boolean;
var
Idx: Integer;
begin
if FKeys.Find(ASymbol.Name, Idx) then
begin
Result := False;
Exit;
end;
FSymbols.Add(ASymbol);
{ Store the TSymbol pointer directly in the string list's Objects slot. }
FKeys.AddObject(ASymbol.Name, ASymbol);
Result := True;
end;
function TScope.LookupLocal(const AName: string): TSymbol;
var
Idx: Integer;
begin
if FKeys.Find(AName, Idx) then
Result := TSymbol(FKeys.Objects[Idx])
else
Result := nil;
end;
function TScope.Lookup(const AName: string): TSymbol;
var
S: TScope;
begin
S := Self;
while S <> nil do
begin
Result := S.LookupLocal(AName);
if Result <> nil then
Exit;
S := S.FParent;
end;
Result := nil;
end;
{ ------------------------------------------------------------------ }
{ TSymbolTable }
{ ------------------------------------------------------------------ }
constructor TSymbolTable.Create;
begin
inherited Create;
FScopeStack := TObjectList.Create(True);
FAllTypes := TObjectList.Create(True);
{ Global scope — parent = nil }
FScopeStack.Add(TScope.Create(nil));
RegisterBuiltins;
end;
destructor TSymbolTable.Destroy;
begin
FScopeStack.Free;
FAllTypes.Free;
inherited Destroy;
end;
function TSymbolTable.NewType(AKind: TTypeKind; const AName: string): TTypeDesc;
begin
Result := TTypeDesc.Create;
Result.Kind := AKind;
Result.Name := AName;
FAllTypes.Add(Result);
end;
procedure TSymbolTable.RegisterBuiltins;
var
Sym: TSymbol;
begin
{ Primitive types }
FTypeInteger := NewType(tyInteger, 'Integer');
FTypeInt64 := NewType(tyInt64, 'Int64');
FTypeUInt32 := NewType(tyUInt32, 'UInt32');
FTypeByte := NewType(tyByte, 'Byte');
FTypeBoolean := NewType(tyBoolean, 'Boolean');
FTypeString := NewType(tyString, 'string');
FTypeVoid := NewType(tyVoid, 'void');
{ Register type names as skType symbols in global scope }
Define(TSymbol.Create('Integer', skType, FTypeInteger));
Define(TSymbol.Create('Int64', skType, FTypeInt64));
Define(TSymbol.Create('UInt32', skType, FTypeUInt32));
Define(TSymbol.Create('Byte', skType, FTypeByte));
Define(TSymbol.Create('Boolean', skType, FTypeBoolean));
Define(TSymbol.Create('string', skType, FTypeString));
{ Built-in I/O procedures }
Sym := TSymbol.Create('Write', skProcedure, nil);
Define(Sym);
Sym := TSymbol.Create('WriteLn', skProcedure, nil);
Define(Sym);
end;
function TSymbolTable.GetCurrentScope: TScope;
begin
Result := TScope(FScopeStack[FScopeStack.Count - 1]);
end;
function TSymbolTable.GetScopeDepth: Integer;
begin
Result := FScopeStack.Count;
end;
function TSymbolTable.PushScope: TScope;
begin
Result := TScope.Create(CurrentScope);
FScopeStack.Add(Result);
end;
procedure TSymbolTable.PopScope;
begin
if FScopeStack.Count > 1 then
FScopeStack.Delete(FScopeStack.Count - 1);
end;
function TSymbolTable.Define(ASymbol: TSymbol): Boolean;
begin
Result := CurrentScope.Define(ASymbol);
end;
function TSymbolTable.Lookup(const AName: string): TSymbol;
begin
Result := CurrentScope.Lookup(AName);
end;
function TSymbolTable.FindType(const AName: string): TTypeDesc;
var
Sym: TSymbol;
begin
Sym := CurrentScope.Lookup(AName);
if (Sym <> nil) and (Sym.Kind = skType) then
Result := Sym.TypeDesc
else
Result := nil;
end;
end.

View file

@ -12,7 +12,8 @@ uses
consoletestrunner,
cp.test.lexer,
cp.test.parser,
cp.test.codegen;
cp.test.codegen,
cp.test.symtable;
var
Application: TTestRunner;

View file

@ -0,0 +1,532 @@
unit cp.test.symtable;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
uSymbolTable;
type
TSymbolTableTests = class(TTestCase)
published
{ Built-in primitive types are pre-registered }
procedure TestBuiltin_Type_Integer;
procedure TestBuiltin_Type_Int64;
procedure TestBuiltin_Type_UInt32;
procedure TestBuiltin_Type_Byte;
procedure TestBuiltin_Type_Boolean;
procedure TestBuiltin_Type_String;
{ Type descriptor properties }
procedure TestTypeDesc_Integer_IsNumeric;
procedure TestTypeDesc_Boolean_IsNotNumeric;
procedure TestTypeDesc_String_IsString;
procedure TestTypeDesc_Integer_IsNotString;
{ FindType is case-insensitive (Pascal identifiers are) }
procedure TestFindType_CaseInsensitive;
procedure TestFindType_Unknown_ReturnsNil;
{ Symbol definition }
procedure TestDefine_Variable;
procedure TestDefine_Procedure;
procedure TestDefine_DuplicateInSameScope_ReturnsFalse;
procedure TestDefine_SameNameDiffScope_IsAllowed;
{ Symbol lookup }
procedure TestLookup_FindsInCurrentScope;
procedure TestLookup_NotFound_ReturnsNil;
procedure TestLookup_CaseInsensitive;
{ Scope nesting }
procedure TestScope_InnerSeesOuter;
procedure TestScope_OuterCannotSeeInner;
procedure TestScope_InnerShadowsOuter;
procedure TestScope_AfterPop_InnerSymbolsGone;
procedure TestScope_DepthAfterPushPop;
{ Built-in procedures }
procedure TestBuiltin_WriteLn_Exists;
procedure TestBuiltin_Write_Exists;
procedure TestBuiltin_WriteLn_IsProcedure;
{ Symbol properties }
procedure TestSymbol_Variable_HasType;
procedure TestSymbol_Procedure_HasVoidReturn;
end;
implementation
{ ------------------------------------------------------------------ }
{ Helpers }
{ ------------------------------------------------------------------ }
function MakeVar(const AName: string; AType: TTypeDesc): TSymbol;
begin
Result := TSymbol.Create(AName, skVariable, AType);
end;
function MakeProc(const AName: string): TSymbol;
begin
Result := TSymbol.Create(AName, skProcedure, nil);
end;
{ ------------------------------------------------------------------ }
{ Built-in types }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestBuiltin_Type_Integer;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('Integer type', ST.FindType('Integer'));
AssertEquals('Integer kind', Ord(tyInteger), Ord(ST.FindType('Integer').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Type_Int64;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('Int64 type', ST.FindType('Int64'));
AssertEquals('Int64 kind', Ord(tyInt64), Ord(ST.FindType('Int64').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Type_UInt32;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('UInt32 type', ST.FindType('UInt32'));
AssertEquals('UInt32 kind', Ord(tyUInt32), Ord(ST.FindType('UInt32').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Type_Byte;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('Byte type', ST.FindType('Byte'));
AssertEquals('Byte kind', Ord(tyByte), Ord(ST.FindType('Byte').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Type_Boolean;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('Boolean type', ST.FindType('Boolean'));
AssertEquals('Boolean kind', Ord(tyBoolean), Ord(ST.FindType('Boolean').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Type_String;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('string type', ST.FindType('string'));
AssertEquals('string kind', Ord(tyString), Ord(ST.FindType('string').Kind));
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Type descriptor properties }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestTypeDesc_Integer_IsNumeric;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertTrue('Integer is numeric', ST.FindType('Integer').IsNumeric);
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestTypeDesc_Boolean_IsNotNumeric;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertFalse('Boolean is not numeric', ST.FindType('Boolean').IsNumeric);
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestTypeDesc_String_IsString;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertTrue('string IsString', ST.FindType('string').IsString);
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestTypeDesc_Integer_IsNotString;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertFalse('Integer IsString', ST.FindType('Integer').IsString);
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ FindType }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestFindType_CaseInsensitive;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('integer lowercase', ST.FindType('integer'));
AssertNotNull('INTEGER uppercase', ST.FindType('INTEGER'));
AssertNotNull('String mixed', ST.FindType('String'));
AssertSame('Same descriptor', ST.FindType('Integer'), ST.FindType('integer'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestFindType_Unknown_ReturnsNil;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNull('unknown type', ST.FindType('Foobar'));
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Symbol definition }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestDefine_Variable;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeVar('x', ST.FindType('Integer'));
AssertTrue('Define succeeds', ST.Define(Sym));
AssertSame('Lookup finds it', Sym, ST.Lookup('x'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestDefine_Procedure;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeProc('Greet');
AssertTrue('Define succeeds', ST.Define(Sym));
AssertEquals('Kind', Ord(skProcedure), Ord(ST.Lookup('Greet').Kind));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestDefine_DuplicateInSameScope_ReturnsFalse;
var
ST: TSymbolTable;
S1, S2: TSymbol;
begin
ST := TSymbolTable.Create;
try
S1 := MakeVar('x', ST.FindType('Integer'));
S2 := MakeVar('x', ST.FindType('string'));
AssertTrue('First define ok', ST.Define(S1));
AssertFalse('Duplicate fails', ST.Define(S2));
S2.Free;
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestDefine_SameNameDiffScope_IsAllowed;
var
ST: TSymbolTable;
S1, S2: TSymbol;
begin
ST := TSymbolTable.Create;
try
S1 := MakeVar('n', ST.FindType('Integer'));
AssertTrue('Outer define ok', ST.Define(S1));
ST.PushScope;
S2 := MakeVar('n', ST.FindType('string'));
AssertTrue('Inner define ok — shadowing allowed', ST.Define(S2));
ST.PopScope;
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Symbol lookup }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestLookup_FindsInCurrentScope;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeVar('result', ST.FindType('Integer'));
ST.Define(Sym);
AssertSame('Found', Sym, ST.Lookup('result'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestLookup_NotFound_ReturnsNil;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNull('Missing symbol', ST.Lookup('doesNotExist'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestLookup_CaseInsensitive;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeVar('MyVar', ST.FindType('Integer'));
ST.Define(Sym);
AssertSame('lowercase', Sym, ST.Lookup('myvar'));
AssertSame('uppercase', Sym, ST.Lookup('MYVAR'));
AssertSame('mixed', Sym, ST.Lookup('MyVar'));
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Scope nesting }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestScope_InnerSeesOuter;
var
ST: TSymbolTable;
Outer: TSymbol;
begin
ST := TSymbolTable.Create;
try
Outer := MakeVar('x', ST.FindType('Integer'));
ST.Define(Outer);
ST.PushScope;
AssertSame('Inner sees outer x', Outer, ST.Lookup('x'));
ST.PopScope;
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestScope_OuterCannotSeeInner;
var
ST: TSymbolTable;
Inner: TSymbol;
begin
ST := TSymbolTable.Create;
try
ST.PushScope;
Inner := MakeVar('local', ST.FindType('Integer'));
ST.Define(Inner);
ST.PopScope;
AssertNull('Outer cannot see inner local', ST.Lookup('local'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestScope_InnerShadowsOuter;
var
ST: TSymbolTable;
Outer, Inner: TSymbol;
begin
ST := TSymbolTable.Create;
try
Outer := MakeVar('n', ST.FindType('Integer'));
ST.Define(Outer);
ST.PushScope;
Inner := MakeVar('n', ST.FindType('string'));
ST.Define(Inner);
AssertSame('Inner n shadows outer n', Inner, ST.Lookup('n'));
ST.PopScope;
AssertSame('After pop, outer n visible again', Outer, ST.Lookup('n'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestScope_AfterPop_InnerSymbolsGone;
var
ST: TSymbolTable;
Inner: TSymbol;
begin
ST := TSymbolTable.Create;
try
ST.PushScope;
Inner := MakeVar('temp', ST.FindType('Integer'));
ST.Define(Inner);
AssertNotNull('Present before pop', ST.Lookup('temp'));
ST.PopScope;
AssertNull('Gone after pop', ST.Lookup('temp'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestScope_DepthAfterPushPop;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertEquals('Depth 1 initially', 1, ST.ScopeDepth);
ST.PushScope;
AssertEquals('Depth 2 after push', 2, ST.ScopeDepth);
ST.PushScope;
AssertEquals('Depth 3', 3, ST.ScopeDepth);
ST.PopScope;
AssertEquals('Depth 2 after pop', 2, ST.ScopeDepth);
ST.PopScope;
AssertEquals('Depth 1 restored', 1, ST.ScopeDepth);
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Built-in procedures }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestBuiltin_WriteLn_Exists;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('WriteLn exists', ST.Lookup('WriteLn'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_Write_Exists;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertNotNull('Write exists', ST.Lookup('Write'));
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestBuiltin_WriteLn_IsProcedure;
var
ST: TSymbolTable;
begin
ST := TSymbolTable.Create;
try
AssertEquals('WriteLn kind',
Ord(skProcedure), Ord(ST.Lookup('WriteLn').Kind));
finally
ST.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Symbol properties }
{ ------------------------------------------------------------------ }
procedure TSymbolTableTests.TestSymbol_Variable_HasType;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeVar('s', ST.FindType('string'));
ST.Define(Sym);
AssertSame('TypeDesc', ST.FindType('string'), ST.Lookup('s').TypeDesc);
finally
ST.Free;
end;
end;
procedure TSymbolTableTests.TestSymbol_Procedure_HasVoidReturn;
var
ST: TSymbolTable;
Sym: TSymbol;
begin
ST := TSymbolTable.Create;
try
Sym := MakeProc('DoSomething');
ST.Define(Sym);
AssertNull('Procedure has no return type', ST.Lookup('DoSomething').TypeDesc);
finally
ST.Free;
end;
end;
initialization
RegisterTest(TSymbolTableTests);
end.