Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions benchmarks/for-in/for-in.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*---
description: for...in enumeration and prototype-chain key dedup benchmarks
---*/

suite("for...in", () => {
// Build a `levels`-deep prototype chain where every level redeclares the
// same `width` keys (plus one unique key per level). for...in must dedup
// the shared keys down the chain, which stresses the key-dedup set.
const buildChain = (levels, width) => {
const sharedKeys = Array.from({ length: width }, (_, i) => "k" + i);
return Array.from({ length: levels }).reduce((proto, _unused, level) => {
const obj = Object.create(proto);
for (const key of sharedKeys) obj[key] = level;
obj["unique" + level] = level;
return obj;
}, null);
};

const flat50 = Array.from({ length: 50 }).reduce((obj, _unused, i) => {
obj["k" + i] = i;
return obj;
}, {});

bench("for...in over 50 own keys", {
run: () => {
let count = 0;
for (const key in flat50) count = count + 1;
return count;
},
});

bench("for...in over an 8-level chain of 50 shared keys", {
setup: () => buildChain(8, 50),
run: (obj) => {
let count = 0;
for (const key in obj) count = count + 1;
return count;
},
});

bench("for...in over a 16-level chain of 100 shared keys", {
setup: () => buildChain(16, 100),
run: (obj) => {
let count = 0;
for (const key in obj) count = count + 1;
return count;
},
});
});
3 changes: 3 additions & 0 deletions benchmarks/for-in/goccia.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"compat-for-in-loop": true
}
12 changes: 7 additions & 5 deletions source/units/Goccia.Evaluator.pas
Original file line number Diff line number Diff line change
Expand Up @@ -5408,7 +5408,7 @@ function CreateForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue;
Keys: TArray<string>;
Key: string;
KeyValue: TGocciaStringLiteralValue;
Visited: TStringList;
Visited: TOrderedStringMap<Boolean>;
GC: TGarbageCollector;
ChainDepth: Integer;
begin
Expand All @@ -5424,9 +5424,11 @@ function CreateForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue;
Obj := ToObject(AValue);
if Assigned(GC) then
GC.AddTempRoot(Obj);
Visited := TStringList.Create;
// Dedup keys across the prototype chain via O(1) hash-set membership
// (native case-sensitive string equality); OrderForInPropertyKeys
// (above) owns per-level enumeration order.
Visited := TOrderedStringMap<Boolean>.Create;
try
Visited.CaseSensitive := True;
Current := Obj;
ChainDepth := 0;
while Assigned(Current) do
Expand All @@ -5439,10 +5441,10 @@ function CreateForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue;
Keys := OrderForInPropertyKeys(Current.GetAllPropertyNames);
for Key in Keys do
begin
if Visited.IndexOf(Key) >= 0 then
if Visited.ContainsKey(Key) then
Continue;

Visited.Add(Key);
Visited.Add(Key, True);
EntryObj := TGocciaObjectValue.Create;
if Assigned(GC) then
GC.AddTempRoot(EntryObj);
Expand Down
13 changes: 8 additions & 5 deletions source/units/Goccia.VM.pas
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ implementation
SysUtils,

BigInteger,
OrderedStringMap,
TextSemantics,
TimingUtils,

Expand Down Expand Up @@ -8425,7 +8426,7 @@ function TGocciaVM.ForInEntriesArray(
Keys: TArray<string>;
Key: string;
KeyValue: TGocciaStringLiteralValue;
Visited: TStringList;
Visited: TOrderedStringMap<Boolean>;
GC: TGarbageCollector;
ChainDepth: Integer;
begin
Expand All @@ -8441,9 +8442,11 @@ function TGocciaVM.ForInEntriesArray(
Obj := ToObject(AValue);
if Assigned(GC) then
GC.AddTempRoot(Obj);
Visited := TStringList.Create;
// Dedup keys across the prototype chain via O(1) hash-set membership
// (native case-sensitive string equality); VMOrderOwnPropertyStringKeys
// (above) owns per-level enumeration order.
Visited := TOrderedStringMap<Boolean>.Create;
try
Visited.CaseSensitive := True;
Current := Obj;
ChainDepth := 0;
while Assigned(Current) do
Expand All @@ -8456,10 +8459,10 @@ function TGocciaVM.ForInEntriesArray(
Keys := VMOrderOwnPropertyStringKeys(Current.GetAllPropertyNames);
for Key in Keys do
begin
if Visited.IndexOf(Key) >= 0 then
if Visited.ContainsKey(Key) then
Continue;

Visited.Add(Key);
Visited.Add(Key, True);
EntryObj := TGocciaObjectValue.Create;
if Assigned(GC) then
GC.AddTempRoot(EntryObj);
Expand Down
55 changes: 55 additions & 0 deletions tests/language/for-in-loop/basic-enumeration.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,58 @@ test("deleted own shadow does not expose inherited property", () => {

expect(keys).toEqual(["a"]);
});

test("enumerable own key shadowing a same-named inherited key is yielded once", () => {
const proto = { shared: "proto", onlyProto: 1 };
const obj = Object.create(proto);
obj.shared = "own";
obj.onlyOwn = 2;

const keys = [];
for (const key in obj) keys.push(key);

// "shared" appears once, in its own (nearer) position; never re-yielded
// from the prototype.
expect(keys).toEqual(["shared", "onlyOwn", "onlyProto"]);
});

test("dedups same-named keys across a deep prototype chain at scale", () => {
// Build a 12-level chain where every level redeclares the same 40 keys,
// plus one level-unique key. Each shared key must be yielded exactly once
// (from the nearest level); per-level order is preserved.
const LEVELS = 12;
const SHARED = 40;
const sharedKeys = Array.from({ length: SHARED }, (_, i) => "k" + i);

const leaf = Array.from({ length: LEVELS }).reduce((proto, _unused, level) => {
const obj = Object.create(proto);
for (const key of sharedKeys) obj[key] = level;
obj["unique" + level] = level;
return obj;
}, null);

const seen = [];
const counts = {};
for (const key in leaf) {
seen.push(key);
counts[key] = (counts[key] || 0) + 1;
}

// Every key yielded exactly once.
for (const key of seen) expect(counts[key]).toBe(1);

// The nearest (leaf) level owns the shared keys, in their declared order,
// before any inherited level-unique keys.
expect(seen.slice(0, SHARED)).toEqual(sharedKeys);

// All shared keys plus one unique key per level, no duplicates.
expect(seen.length).toBe(SHARED + LEVELS);

// The inherited level-unique keys follow, ordered nearest (leaf) to
// farthest — one per level, guarding against prototype-level reordering.
const expectedUnique = Array.from({ length: LEVELS }, (_, i) => "unique" + (LEVELS - 1 - i));
expect(seen.slice(SHARED)).toEqual(expectedUnique);

// Shared keys resolve to the leaf level's value.
expect(leaf.k0).toBe(LEVELS - 1);
});
Loading