DEADSOFTWARE

mapdef cleanup; renamed some fields; mapdef.txt is RC0 now
[d2df-sdl.git] / src / shared / hashtable.pas
index c649347fbd1b1e87776b961b951d92112ade16e4..8aaa522a45bae64530243a8fa031f42b4b035b4f 100644 (file)
@@ -37,15 +37,54 @@ type
   public
     type THashFn = function (constref o: KeyT): LongWord;
     type TEquFn = function (constref a, b: KeyT): Boolean;
+    type TIteratorFn = function (constref k: KeyT; constref v: ValueT): Boolean is nested; // return `true` to stop
 
-  private
     type
       PEntry = ^TEntry;
       TEntry = record
+      public
         key: KeyT;
         value: ValueT;
+      private
         hash: LongWord; // key hash or 0
-        nextFree: PEntry;
+        nextFree: PEntry; // next free entry
+      end;
+
+  private
+    type
+      TEntryArray = array of TEntry;
+
+      TValEnumerator = record
+      private
+        mEntries: TEntryArray;
+        mFirstEntry, mLastEntry, cur: Integer;
+      public
+        constructor Create (const aents: TEntryArray; afirst, alast: Integer);
+        function MoveNext (): Boolean; inline;
+        function getCurrent (): ValueT; inline;
+        property Current: ValueT read getCurrent;
+      end;
+
+      TKeyEnumerator = record
+      private
+        mEntries: TEntryArray;
+        mFirstEntry, mLastEntry, cur: Integer;
+      public
+        constructor Create (const aents: TEntryArray; afirst, alast: Integer);
+        function MoveNext (): Boolean; inline;
+        function getCurrent (): KeyT; inline;
+        property Current: KeyT read getCurrent;
+      end;
+
+      TKeyValEnumerator = record
+      private
+        mEntries: TEntryArray;
+        mFirstEntry, mLastEntry, cur: Integer;
+      public
+        constructor Create (const aents: TEntryArray; afirst, alast: Integer);
+        function MoveNext (): Boolean; inline;
+        function getCurrent (): PEntry; inline;
+        property Current: PEntry read getCurrent;
       end;
 
   private
@@ -53,9 +92,12 @@ type
     equfn: TEquFn;
     mBuckets: array of PEntry; // entries, points to mEntries elements
     mBucketsUsed: Integer;
-    mEntries: array of TEntry;
+    mEntries: TEntryArray;
+    {$IFDEF RBHASH_SANITY_CHECKS}
     mEntriesUsed: Integer;
+    {$ENDIF}
     mFreeEntryHead: PEntry;
+    mFirstEntry, mLastEntry: Integer;
     mSeed: LongWord;
 
   private
@@ -73,6 +115,7 @@ type
     destructor Destroy (); override;
 
     procedure clear ();
+    procedure reset (); // don't shrink buckets
 
     procedure rehash ();
     procedure compact (); // call this instead of `rehash()` after alot of deletions
@@ -82,11 +125,19 @@ type
     function has (constref akey: KeyT): Boolean; // `true`: found
     function del (constref akey: KeyT): Boolean; // `true`: deleted
 
+    //WARNING! don't modify table in iterator (queries are ok, though)
+    function forEach (it: TIteratorFn): Boolean;
+
+    // default `for ... in` enums values
+    function GetEnumerator (): TValEnumerator;
+    function byKey (): TKeyEnumerator;
+    function byValue (): TValEnumerator;
+    function byKeyValue (): TKeyValEnumerator; // PEntry
+
     property count: Integer read mBucketsUsed;
     property capacity: Integer read getCapacity;
   end;
 
-
 type
   TJoaatHasher = record
   private
@@ -99,7 +150,7 @@ type
     procedure reset (); inline; overload;
     procedure reset (aseed: LongWord); inline; overload;
 
-    procedure put (const buf; len: LongWord);
+    procedure put (constref buf; len: LongWord);
 
     // current hash value
     // you can continue putting data, as this is not destructive
@@ -109,17 +160,28 @@ type
 
 type
   THashIntInt = specialize THashBase<Integer, Integer>;
+  THashStrInt = specialize THashBase<AnsiString, Integer>;
+  THashStrStr = specialize THashBase<AnsiString, AnsiString>;
 
 function hashNewIntInt (): THashIntInt;
+function hashNewStrInt (): THashStrInt;
+function hashNewStrStr (): THashStrStr;
 
 
 function u32Hash (a: LongWord): LongWord; inline;
-function fnvHash (const buf; len: LongWord): LongWord;
-function joaatHash (const buf; len: LongWord): LongWord;
+function fnvHash (constref buf; len: LongWord): LongWord;
+function joaatHash (constref buf; len: LongWord): LongWord;
 
 function nextPOT (x: LongWord): LongWord; inline;
 
 
+// for integer keys
+function hiiequ (constref a, b: Integer): Boolean;
+function hiihash (constref k: Integer): LongWord;
+function hsiequ (constref a, b: AnsiString): Boolean;
+function hsihash (constref k: AnsiString): LongWord;
+
+
 implementation
 
 uses
@@ -145,12 +207,13 @@ end;
 
 // ////////////////////////////////////////////////////////////////////////// //
 function hiiequ (constref a, b: Integer): Boolean; begin result := (a = b); end;
+function hsiequ (constref a, b: AnsiString): Boolean; begin result := (a = b); end;
 
 {$PUSH}
 {$RANGECHECKS OFF}
 function hiihash (constref k: Integer): LongWord;
 begin
-  result := k;
+  result := LongWord(k);
   result -= (result shl 6);
   result := result xor (result shr 17);
   result -= (result shl 9);
@@ -159,6 +222,11 @@ begin
   result := result xor (result shl 10);
   result := result xor (result shr 15);
 end;
+
+function hsihash (constref k: AnsiString): LongWord;
+begin
+  if (Length(k) > 0) then result := fnvHash(PAnsiChar(k)^, Length(k)) else result := 0;
+end;
 {$POP}
 
 
@@ -168,6 +236,18 @@ begin
 end;
 
 
+function hashNewStrInt (): THashStrInt;
+begin
+  result := THashStrInt.Create(hsihash, hsiequ);
+end;
+
+
+function hashNewStrStr (): THashStrStr;
+begin
+  result := THashStrStr.Create(hsihash, hsiequ);
+end;
+
+
 // ////////////////////////////////////////////////////////////////////////// //
 {$PUSH}
 {$RANGECHECKS OFF}
@@ -190,7 +270,7 @@ begin
 end;
 
 
-procedure TJoaatHasher.put (const buf; len: LongWord);
+procedure TJoaatHasher.put (constref buf; len: LongWord);
 var
   bytes: PByte;
   h: LongWord;
@@ -220,12 +300,12 @@ end;
 {$POP}
 
 
-function joaatHash (const buf; len: LongWord): LongWord;
+function joaatHash (constref buf; len: LongWord): LongWord;
 var
   h: TJoaatHasher;
 begin
   h := TJoaatHasher.Create(0);
-  h.put(buf, len);
+  h.put(PByte(@buf)^, len);
   result := h.value;
 end;
 
@@ -234,7 +314,7 @@ end;
 {$PUSH}
 {$RANGECHECKS OFF}
 // fnv-1a: http://www.isthe.com/chongo/tech/comp/fnv/
-function fnvHash (const buf; len: LongWord): LongWord;
+function fnvHash (constref buf; len: LongWord): LongWord;
 var
   b: PByte;
 begin
@@ -289,39 +369,182 @@ begin
 end;
 
 
+procedure THashBase.clear ();
+var
+  idx: Integer;
+begin
+  SetLength(mBuckets, InitSize);
+  for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
+
+  SetLength(mEntries, Length(mBuckets));
+  {
+  for idx := 0 to High(mEntries)-1 do
+  begin
+    mEntries[idx].hash := 0;
+    mEntries[idx].nextFree := @mEntries[idx+1]; //idx+1;
+  end;
+  mEntries[High(mEntries)].hash := 0;
+  mEntries[High(mEntries)].nextFree := nil;
+  }
+  {
+  for idx := 0 to High(mEntries) do
+  begin
+    mEntries[idx].hash := 0;
+    mEntries[idx].nextFree := nil;
+  end;
+  }
+
+  mBucketsUsed := 0;
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  mEntriesUsed := 0;
+  {$ENDIF}
+  mFreeEntryHead := nil; //@mEntries[0];
+  mFirstEntry := -1;
+  mLastEntry := -1;
+end;
+
+
+procedure THashBase.reset ();
+var
+  idx: Integer;
+begin
+  if (mBucketsUsed > 0) then
+  begin
+    for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
+    {
+    for idx := 0 to High(mEntries)-1 do
+    begin
+      mEntries[idx].hash := 0;
+      mEntries[idx].nextFree := @mEntries[idx+1]; //idx+1;
+    end;
+    mEntries[High(mEntries)].hash := 0;
+    mEntries[High(mEntries)].nextFree := nil;
+    }
+    {
+    if (mFirstEntry >= 0) then
+    begin
+      for idx := mFirstEntry to mLastEntry do
+      begin
+        mEntries[idx].hash := 0;
+        mEntries[idx].nextFree := nil;
+      end;
+    end;
+    }
+
+    mBucketsUsed := 0;
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    mEntriesUsed := 0;
+    {$ENDIF}
+    mFreeEntryHead := nil; //@mEntries[0];
+    mFirstEntry := -1;
+    mLastEntry := -1;
+  end;
+end;
+
+
 function THashBase.getCapacity (): Integer; inline; begin result := Length(mBuckets); end;
 
 
 function THashBase.allocEntry (): PEntry;
+var
+  idx: Integer;
 begin
+  if (mFreeEntryHead = nil) then
+  begin
+    if (mLastEntry = High(mEntries)) then raise Exception.Create('internal error in hash entry allocator (0.0)');
+    Inc(mLastEntry);
+    if (mFirstEntry = -1) then
+    begin
+      if (mLastEntry <> 0) then raise Exception.Create('internal error in hash entry allocator (0.1)');
+      mFirstEntry := 0;
+    end;
+    result := @mEntries[mLastEntry];
+    result.nextFree := nil; // just in case
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    Inc(mEntriesUsed);
+    {$ENDIF}
+    exit;
+  end;
   {$IFDEF RBHASH_SANITY_CHECKS}
   if (mFreeEntryHead = nil) then raise Exception.Create('internal error in hash entry allocator (0)');
   if (mFreeEntryHead.hash <> 0) then raise Exception.Create('internal error in hash entry allocator (1)');
   {$ENDIF}
   result := mFreeEntryHead;
   mFreeEntryHead := result.nextFree;
+  {$IFDEF RBHASH_SANITY_CHECKS}
   Inc(mEntriesUsed);
-  result.nextFree := nil;
+  {$ENDIF}
+  result.nextFree := nil; // just in case
+  // fix mFirstEntry and mLastEntry
+  idx := Integer((PtrUInt(result)-PtrUInt(@mEntries[0])) div sizeof(mEntries[0]));
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  if (idx < 0) or (idx > High(mEntries)) then raise Exception.Create('internal error in hash entry allocator (invalid entry address)');
+  if (result <> @mEntries[idx]) then raise Exception.Create('internal error in hash entry allocator (wtf?!)');
+  {$ENDIF}
+  if (mFirstEntry < 0) or (idx < mFirstEntry) then mFirstEntry := idx;
+  if (idx > mLastEntry) then mLastEntry := idx;
 end;
 
 
 procedure THashBase.releaseEntry (e: PEntry);
-//var
-//  idx: LongWord;
+var
+  cidx, idx: Integer;
 begin
   {$IFDEF RBHASH_SANITY_CHECKS}
   if (mEntriesUsed = 0) then raise Exception.Create('internal error in hash entry allocator');
+  if (mEntriesUsed <> mBucketsUsed) then raise Exception.Create('internal error in hash entry allocator (entry/bucket count mismatch)');
   if (e = nil) then raise Exception.Create('internal error in hash entry allocator (trying to release nil entry)');
-  if (e.nextFree <> nil) or (e.hash = 0) then raise Exception.Create('internal error in hash entry allocator (trying to release unallocated entry)');
+  if (e.hash = 0) then raise Exception.Create('internal error in hash entry allocator (trying to release unallocated entry)');
   {$ENDIF}
-  //idx := LongWord((PtrUInt(e)-PtrUInt(@mEntries[0])) div sizeof(mEntries[0]));
+  idx := Integer((PtrUInt(e)-PtrUInt(@mEntries[0])) div sizeof(mEntries[0]));
   {$IFDEF RBHASH_SANITY_CHECKS}
-  //if (idx >= Length(mEntries)) then raise Exception.Create('internal error in hash entry allocator (invalid calculated index)');
+  if (idx < 0) or (idx > High(mEntries)) then raise Exception.Create('internal error in hash entry allocator (invalid entry address)');
+  if (e <> @mEntries[idx]) then raise Exception.Create('internal error in hash entry allocator (wtf?!)');
+  {$ENDIF}
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  Dec(mEntriesUsed);
   {$ENDIF}
   e.hash := 0;
   e.nextFree := mFreeEntryHead;
   mFreeEntryHead := e; //idx;
-  Dec(mEntriesUsed);
+  // fix mFirstEntry and mLastEntry
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  if (mFirstEntry < 0) or (mLastEntry < 0) then raise Exception.Create('internal error in hash entry allocator (invalid first/last range; 0)');
+  {$ENDIF}
+  if (mFirstEntry = mLastEntry) then
+  begin
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    if (mEntriesUsed <> 0) then raise Exception.Create('internal error in hash entry allocator (invalid first/last range; 1)');
+    {$ENDIF}
+    mFirstEntry := -1;
+    mLastEntry := -1;
+  end
+  else
+  begin
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    if (mEntriesUsed = 0) then raise Exception.Create('internal error in hash entry allocator (invalid first/last range; 2)');
+    {$ENDIF}
+    // fix first entry index
+    if (idx = mFirstEntry) then
+    begin
+      cidx := idx+1;
+      while (mEntries[cidx].hash = 0) do Inc(cidx);
+      {$IFDEF RBHASH_SANITY_CHECKS}
+      if (cidx > High(mEntries)) then raise Exception.Create('internal error in hash entry allocator (invalid first/last range; 3)');
+      {$ENDIF}
+      mFirstEntry := cidx;
+    end;
+    // fix last entry index
+    if (idx = mLastEntry) then
+    begin
+      cidx := idx-1;
+      while (mEntries[cidx].hash = 0) do Dec(cidx);
+      {$IFDEF RBHASH_SANITY_CHECKS}
+      if (cidx < 0) then raise Exception.Create('internal error in hash entry allocator (invalid first/last range; 3)');
+      {$ENDIF}
+      mLastEntry := cidx;
+    end;
+  end;
 end;
 
 
@@ -487,7 +710,7 @@ begin
     {$ENDIF}
     SetLength(mBuckets, newsz);
     // resize entries array
-    eidx := newsz;
+    eidx := Length(mEntries);
     SetLength(mEntries, newsz);
     while (eidx < Length(mEntries)) do begin mEntries[eidx].hash := 0; Inc(eidx); end;
     // mFreeEntryHead will be fixed in `rehash()`
@@ -571,33 +794,14 @@ begin
 end;
 
 
-procedure THashBase.clear ();
-var
-  idx: Integer;
-begin
-  SetLength(mBuckets, InitSize);
-  for idx := 0 to High(mBuckets) do mBuckets[idx] := nil;
-
-  SetLength(mEntries, Length(mBuckets));
-  for idx := 0 to High(mEntries)-1 do
-  begin
-    mEntries[idx].hash := 0;
-    mEntries[idx].nextFree := @mEntries[idx+1]; //idx+1;
-  end;
-  mEntries[High(mEntries)].hash := 0;
-  mEntries[High(mEntries)].nextFree := nil;
-
-  mBucketsUsed := 0;
-  mEntriesUsed := 0;
-  mFreeEntryHead := @mEntries[0];
-end;
-
-
 procedure THashBase.rehash ();
 var
   idx: Integer;
   lastfree: PEntry;
-  e: PEntry;
+  e: PEntry = nil; // shut up, fpc!
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  cnt: Integer = 0;
+  {$ENDIF}
 begin
   // change seed, to minimize pathological cases
   if (mSeed = 0) then mSeed := $29a;
@@ -613,6 +817,12 @@ begin
     e := @mEntries[idx];
     if (e.hash <> 0) then
     begin
+      {$IFDEF RBHASH_SANITY_CHECKS}
+      if (e.nextFree <> nil) then raise Exception.Create('internal error in rehash: inconsistent');
+      if (cnt = 0) and (idx <> mFirstEntry) then raise Exception.Create('internal error in rehash: inconsistent (1)');
+      Inc(cnt);
+      if (cnt = mBucketsUsed) and (idx <> mLastEntry) then raise Exception.Create('internal error in rehash: inconsistent (2)');
+      {$ENDIF}
       e.hash := hashfn(e.key) xor mSeed; if (e.hash = 0) then e.hash := $29a;
       putEntryInternal(e);
     end
@@ -622,12 +832,20 @@ begin
       lastfree := e;
     end;
   end;
+  if (lastfree <> nil) then e.nextFree := nil;
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  if (cnt <> mBucketsUsed) then raise Exception.Create('internal error in hash table resize (invalid first/last range; 0)');
+  if (cnt <> mEntriesUsed) then raise Exception.Create('internal error in hash table resize (invalid first/last range; 1)');
+  {$ENDIF}
 end;
 
 
 procedure THashBase.compact ();
 var
   newsz, didx, f: Integer;
+  {$IFDEF RBHASH_SANITY_CHECKS}
+  cnt: Integer;
+  {$ENDIF}
 begin
   newsz := nextPOT(LongWord(mBucketsUsed));
   if (newsz >= 1024*1024*1024) then exit;
@@ -636,27 +854,55 @@ begin
   {$IFDEF RBHASH_DEBUG_COMPACT}writeln('compacting; used=', mBucketsUsed, '; oldsizePOT=', newsz, '; newsize=', newsz*2);{$ENDIF}
   newsz *= 2;
   // move all entries to top
-  didx := 0;
-  while (didx < Length(mEntries)) do if (mEntries[didx].hash <> 0) then Inc(didx) else break;
-  f := didx+1;
-  while (f < Length(mEntries)) do if (mEntries[f].hash = 0) then Inc(f) else break;
-  // copy entries
-  while (f < Length(mEntries)) do
+  if (mFirstEntry >= 0) then
   begin
-    if (mEntries[f].hash <> 0) then
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    if (mBucketsUsed < 1) then raise Exception.Create('internal error in hash table (invalid bucket count; 0)');
+    {$ENDIF}
+    didx := 0;
+    while (didx < Length(mEntries)) do if (mEntries[didx].hash <> 0) then Inc(didx) else break;
+    f := didx+1;
+    // copy entries
+    while true do
     begin
-      if (didx >= f) then raise Exception.Create('internal error in hash: inconsistent');
-      mEntries[didx] := mEntries[f];
-      mEntries[f].hash := 0;
-      Inc(didx);
-      while (didx < Length(mEntries)) do if (mEntries[didx].hash <> 0) then Inc(didx) else break;
+      if (mEntries[f].hash <> 0) then
+      begin
+        {$IFDEF RBHASH_SANITY_CHECKS}
+        if (didx >= f) then raise Exception.Create('internal error in hash: inconsistent');
+        {$ENDIF}
+        mEntries[didx] := mEntries[f];
+        mEntries[f].hash := 0;
+        Inc(didx);
+        if (f = mLastEntry) then break;
+        while (didx < Length(mEntries)) do if (mEntries[didx].hash <> 0) then Inc(didx) else break;
+      end;
       Inc(f);
-      while (f < Length(mEntries)) do if (mEntries[f].hash = 0) then Inc(f) else break;
-    end
-    else
+    end;
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    if (didx <> mBucketsUsed) then raise Exception.Create('internal error in hash table (invalid first/last range; 1)');
+    {$ENDIF}
+    mFirstEntry := 0;
+    mLastEntry := mBucketsUsed-1;
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    cnt := 0;
+    for f := mFirstEntry to mLastEntry do
     begin
-      Inc(f);
+      if (mEntries[f].hash = 0) then raise Exception.Create('internal error in hash table (invalid first/last range; 2)');
+      Inc(cnt);
+    end;
+    if (cnt <> mBucketsUsed) then raise Exception.Create('internal error in hash table (invalid first/last range; 3)');
+    if (cnt <> mEntriesUsed) then raise Exception.Create('internal error in hash table (invalid first/last range; 4)');
+    for f := mLastEntry+1 to High(mEntries) do
+    begin
+      if (mEntries[f].hash <> 0) then raise Exception.Create('internal error in hash table (invalid first/last range; 5)');
     end;
+    {$ENDIF}
+  end
+  else
+  begin
+    {$IFDEF RBHASH_SANITY_CHECKS}
+    if (mBucketsUsed <> 0) then raise Exception.Create('internal error in hash table (invalid bucket count; 1)');
+    {$ENDIF}
   end;
   // shrink
   SetLength(mBuckets, newsz);
@@ -667,4 +913,125 @@ begin
 end;
 
 
+function THashBase.forEach (it: TIteratorFn): Boolean;
+var
+  i: Integer;
+begin
+  result := false;
+  if not assigned(it) then exit;
+  i := mFirstEntry;
+  if (i < 0) then exit;
+  while (i <= mLastEntry) do
+  begin
+    if (mEntries[i].hash <> 0) then
+    begin
+      result := it(mEntries[i].key, mEntries[i].value);
+      if result then exit;
+    end;
+    Inc(i);
+  end;
+end;
+
+
+// enumerators
+function THashBase.GetEnumerator (): TValEnumerator;
+begin
+  if (Length(mEntries) > 0) then result := TValEnumerator.Create(mEntries, mFirstEntry, mLastEntry)
+  else result := TValEnumerator.Create(nil, -1, -1);
+end;
+
+function THashBase.byKey (): TKeyEnumerator;
+begin
+  if (Length(mEntries) > 0) then result := TKeyEnumerator.Create(mEntries, mFirstEntry, mLastEntry)
+  else result := TKeyEnumerator.Create(nil, -1, -1);
+end;
+
+function THashBase.byValue (): TValEnumerator;
+begin
+  if (Length(mEntries) > 0) then result := TValEnumerator.Create(mEntries, mFirstEntry, mLastEntry)
+  else result := TValEnumerator.Create(nil, -1, -1);
+end;
+
+function THashBase.byKeyValue (): TKeyValEnumerator; // PEntry
+begin
+  if (Length(mEntries) > 0) then result := TKeyValEnumerator.Create(mEntries, mFirstEntry, mLastEntry)
+  else result := TKeyValEnumerator.Create(nil, -1, -1);
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THashBase.TValEnumerator.Create (const aents: TEntryArray; afirst, alast: Integer);
+begin
+  mEntries := aents;
+  mFirstEntry := afirst;
+  mLastEntry := alast;
+  cur := mFirstEntry-1;
+end;
+
+function THashBase.TValEnumerator.MoveNext (): Boolean; inline;
+begin
+  Inc(cur);
+  while (cur <= mLastEntry) do
+  begin
+    if (mEntries[cur].hash <> 0) then begin result := true; exit; end;
+  end;
+  result := false;
+end;
+
+function THashBase.TValEnumerator.getCurrent (): ValueT; inline;
+begin
+  result := mEntries[cur].value;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THashBase.TKeyEnumerator.Create (const aents: TEntryArray; afirst, alast: Integer);
+begin
+  mEntries := aents;
+  mFirstEntry := afirst;
+  mLastEntry := alast;
+  cur := mFirstEntry-1;
+end;
+
+function THashBase.TKeyEnumerator.MoveNext (): Boolean; inline;
+begin
+  Inc(cur);
+  while (cur <= mLastEntry) do
+  begin
+    if (mEntries[cur].hash <> 0) then begin result := true; exit; end;
+  end;
+  result := false;
+end;
+
+function THashBase.TKeyEnumerator.getCurrent (): KeyT; inline;
+begin
+  result := mEntries[cur].key;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THashBase.TKeyValEnumerator.Create (const aents: TEntryArray; afirst, alast: Integer);
+begin
+  mEntries := aents;
+  mFirstEntry := afirst;
+  mLastEntry := alast;
+  cur := mFirstEntry-1;
+end;
+
+function THashBase.TKeyValEnumerator.MoveNext (): Boolean; inline;
+begin
+  Inc(cur);
+  while (cur <= mLastEntry) do
+  begin
+    if (mEntries[cur].hash <> 0) then begin result := true; exit; end;
+  end;
+  result := false;
+end;
+
+function THashBase.TKeyValEnumerator.getCurrent (): PEntry; inline;
+begin
+  result := @mEntries[cur];
+end;
+
+
 end.