Skip to content

Add stucturedClone#24

Merged
frostney merged 5 commits into
mainfrom
feat-structured-clone
Feb 21, 2026
Merged

Add stucturedClone#24
frostney merged 5 commits into
mainfrom
feat-structured-clone

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 20, 2026

Summary by CodeRabbit

  • New Features

    • Added structuredClone() as a global API for deep-cloning values, preserving shared and circular references; throws for non-cloneable types.
  • Documentation

    • Updated API docs with usage, behavior for primitives, circular/shared references, and note on accessor preservation.
  • Tests

    • Added comprehensive test suites validating primitives, objects, arrays, Map/Set, circular/shared references, and error/argument handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Adds a new global function structuredClone(value) to GocciaScript: implements deep cloning for primitives, objects, arrays, Map, and Set with memoization to preserve shared/circular references; throws on non-cloneable types; registers the binding and adds documentation plus comprehensive tests.

Changes

Cohort / File(s) Summary
Documentation
docs/built-ins.md
Added structuredClone(value) entry and explanatory text about supported types, preservation of primitives, shared/circular reference handling, and error behavior for non-cloneable types.
Runtime implementation
units/Goccia.Builtins.Globals.pas
Added StructuredCloneCallback and supporting routines (StructuredCloneValue, CloneObject, CloneArray, CloneMap, CloneSet) with per-call memoization; updated uses/dependencies and registered the global lexical binding.
Tests — primitives & objects
tests/built-ins/structuredClone/primitives.js, tests/built-ins/structuredClone/objects.js, tests/built-ins/structuredClone/arrays.js
Added tests verifying cloning of primitive values, simple and nested objects, arrays, and that clones are independent (mutations do not affect originals).
Tests — collections & circulars
tests/built-ins/structuredClone/collections.js, tests/built-ins/structuredClone/circular-references.js
Added Map and Set cloning tests and extensive circular/shared-reference scenarios to ensure identity preservation within clones.
Tests — errors & args
tests/built-ins/structuredClone/errors.js
Added tests asserting errors for non-cloneable inputs (functions, symbols), missing arguments, and that a second options argument is accepted/ignored.

Sequence Diagram(s)

sequenceDiagram
  participant Script
  participant Globals
  participant Cloner
  participant MemoryMap
  Script->>Globals: call structuredClone(value)
  Globals->>Cloner: StructuredCloneValue(value, MemoryMap)
  Cloner->>MemoryMap: check existing mapping
  alt mapping exists
    MemoryMap-->>Cloner: return existing clone
  else mapping not found
    Cloner->>Cloner: clone based on type (Object/Array/Map/Set/Primitive)
    Cloner->>MemoryMap: record original -> clone
    opt composite elements
      Cloner->>Cloner: recursively StructuredCloneValue(child, MemoryMap)
    end
    Cloner-->>Globals: return cloned value
  end
  Globals-->>Script: return cloned value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble code in moonlit time,
I copy fields without a crime.
Circles kept where circles were,
Maps and Sets hop safe and sure,
A fresh clone bounds — one, two, three!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title contains a typo: 'stucturedClone' should be 'structuredClone'. Despite this, the title clearly indicates the main change is adding the structuredClone function, which matches the changeset. Correct the typo in the title from 'stucturedClone' to 'structuredClone' to ensure clarity and accuracy.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-structured-clone

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Line 47: The file imports the unused unit Goccia.CallStack which causes
compilation failure; remove Goccia.CallStack from the uses clause in
units/Goccia.Builtins.Globals.pas (ensure there are no references to
Goccia.CallStack elsewhere in that unit), recompile, and if any functionality
from Goccia.CallStack is actually required, replace the import with the correct
unit or add the necessary code to reference it (search for usages of CallStack
or symbols from Goccia.CallStack to verify).

Comment thread units/Goccia.Builtins.Globals.pas Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
units/Goccia.Builtins.Globals.pas (1)

434-438: Consider GC temp root protection for intermediate cloned values.

During structured cloning, newly created values are held only in Pascal code (the Memory dictionary) until the final result is returned. If GC runs mid-clone, these intermediate values could potentially be collected.

Based on learnings: "Values held only by Pascal code (not in any GocciaScript scope) must be protected with AddTempRoot/RemoveTempRoot for the duration they are needed."

♻️ Suggested pattern
 function TGocciaGlobals.StructuredCloneCallback(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 var
   Memory: TDictionary<TGocciaValue, TGocciaValue>;
+  ClonedValue: TGocciaValue;
 begin
   if AArgs.Length = 0 then
     ThrowTypeError('Failed to execute ''structuredClone'': 1 argument required, but only 0 present.');

   Memory := TDictionary<TGocciaValue, TGocciaValue>.Create;
   try
-    Result := StructuredCloneValue(AArgs.GetElement(0), Memory);
+    Result := StructuredCloneValue(AArgs.GetElement(0), Memory);
+    { Protect all cloned values until returned }
+    for ClonedValue in Memory.Values do
+      if Assigned(TGocciaGarbageCollector.Instance) then
+        TGocciaGarbageCollector.Instance.AddTempRoot(ClonedValue);
   finally
+    for ClonedValue in Memory.Values do
+      if Assigned(TGocciaGarbageCollector.Instance) then
+        TGocciaGarbageCollector.Instance.RemoveTempRoot(ClonedValue);
     Memory.Free;
   end;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 434 - 438, The intermediate
cloned values stored only in the Pascal-side Memory dictionary can be collected
by the GC; update the cloning logic so every TGocciaValue instance created and
placed into Memory is registered with AddTempRoot and later unregistered with
RemoveTempRoot when that entry is discarded, and ensure the final value returned
from StructuredCloneValue (the Result) is AddTempRoot-protected before
Memory.Free and only RemoveTempRoot when the engine no longer needs it; apply
these changes inside StructuredCloneValue and around the Memory/Result use to
call AddTempRoot/RemoveTempRoot for each value stored in Memory and for Result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Line 324: The TStringList returned by AObj.GetOwnPropertyKeys is assigned to
Keys but never freed, causing a memory leak; wrap the use of Keys in a
try..finally (or ensure Keys.Free is called) so the list is freed after
processing — locate the assignment to Keys (Keys := AObj.GetOwnPropertyKeys) and
surrounding code in the routine, and change it to obtain Keys, use it, and then
call Keys.Free in a finally block to ensure proper cleanup.
- Around line 337-342: The code currently copies accessor descriptors
(TGocciaPropertyDescriptorAccessor) into the cloned object (via
Result.DefineProperty), but per the structured clone algorithm accessor
properties must cause a DataCloneError because their getter/setter functions are
non-serializable; change the branch that handles
TGocciaPropertyDescriptorAccessor (and any code path that would call
TGocciaPropertyDescriptorAccessor.Create) to raise/throw a DataCloneError
instead of defining the property (use the project's DataCloneError mechanism or
throw an appropriate EDataCloneError), referencing
TGocciaPropertyDescriptorAccessor, Descriptor, Keys[I], and
Result.DefineProperty to locate the code to modify.

---

Duplicate comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Line 47: Remove the unused unit import Goccia.CallStack from the uses clause
in the unit that declares the global builtins (the reference to Goccia.CallStack
is never used), update the uses list so only referenced units remain, and
recompile to ensure the compilation error is resolved; specifically edit the
uses clause where Goccia.CallStack is listed and delete that identifier.

---

Nitpick comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Around line 434-438: The intermediate cloned values stored only in the
Pascal-side Memory dictionary can be collected by the GC; update the cloning
logic so every TGocciaValue instance created and placed into Memory is
registered with AddTempRoot and later unregistered with RemoveTempRoot when that
entry is discarded, and ensure the final value returned from
StructuredCloneValue (the Result) is AddTempRoot-protected before Memory.Free
and only RemoveTempRoot when the engine no longer needs it; apply these changes
inside StructuredCloneValue and around the Memory/Result use to call
AddTempRoot/RemoveTempRoot for each value stored in Memory and for Result.

Result := TGocciaObjectValue.Create;
AMemory.Add(AObj, Result);

Keys := AObj.GetOwnPropertyKeys;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Memory leak: Keys TStringList is never freed.

GetOwnPropertyKeys returns a TStringList that must be freed after use.

🐛 Proposed fix
 function CloneObject(const AObj: TGocciaObjectValue;
   const AMemory: TDictionary<TGocciaValue, TGocciaValue>): TGocciaObjectValue;
 var
   I: Integer;
   Keys: TStringList;
   Descriptor: TGocciaPropertyDescriptor;
   ClonedValue: TGocciaValue;
 begin
   Result := TGocciaObjectValue.Create;
   AMemory.Add(AObj, Result);

   Keys := AObj.GetOwnPropertyKeys;
+  try
     for I := 0 to Keys.Count - 1 do
     begin
       Descriptor := AObj.GetOwnPropertyDescriptor(Keys[I]);
       if not Assigned(Descriptor) then
         Continue;

       if Descriptor is TGocciaPropertyDescriptorData then
       begin
         ClonedValue := StructuredCloneValue(TGocciaPropertyDescriptorData(Descriptor).Value, AMemory);
         Result.DefineProperty(Keys[I],
           TGocciaPropertyDescriptorData.Create(ClonedValue, Descriptor.Flags));
       end
       else if Descriptor is TGocciaPropertyDescriptorAccessor then
         Result.DefineProperty(Keys[I],
           TGocciaPropertyDescriptorAccessor.Create(
             TGocciaPropertyDescriptorAccessor(Descriptor).Getter,
             TGocciaPropertyDescriptorAccessor(Descriptor).Setter,
             Descriptor.Flags));
     end;
+  finally
+    Keys.Free;
+  end;
 end;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Keys := AObj.GetOwnPropertyKeys;
function CloneObject(const AObj: TGocciaObjectValue;
const AMemory: TDictionary<TGocciaValue, TGocciaValue>): TGocciaObjectValue;
var
I: Integer;
Keys: TStringList;
Descriptor: TGocciaPropertyDescriptor;
ClonedValue: TGocciaValue;
begin
Result := TGocciaObjectValue.Create;
AMemory.Add(AObj, Result);
Keys := AObj.GetOwnPropertyKeys;
try
for I := 0 to Keys.Count - 1 do
begin
Descriptor := AObj.GetOwnPropertyDescriptor(Keys[I]);
if not Assigned(Descriptor) then
Continue;
if Descriptor is TGocciaPropertyDescriptorData then
begin
ClonedValue := StructuredCloneValue(TGocciaPropertyDescriptorData(Descriptor).Value, AMemory);
Result.DefineProperty(Keys[I],
TGocciaPropertyDescriptorData.Create(ClonedValue, Descriptor.Flags));
end
else if Descriptor is TGocciaPropertyDescriptorAccessor then
Result.DefineProperty(Keys[I],
TGocciaPropertyDescriptorAccessor.Create(
TGocciaPropertyDescriptorAccessor(Descriptor).Getter,
TGocciaPropertyDescriptorAccessor(Descriptor).Setter,
Descriptor.Flags));
end;
finally
Keys.Free;
end;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` at line 324, The TStringList returned by
AObj.GetOwnPropertyKeys is assigned to Keys but never freed, causing a memory
leak; wrap the use of Keys in a try..finally (or ensure Keys.Free is called) so
the list is freed after processing — locate the assignment to Keys (Keys :=
AObj.GetOwnPropertyKeys) and surrounding code in the routine, and change it to
obtain Keys, use it, and then call Keys.Free in a finally block to ensure proper
cleanup.

Comment thread units/Goccia.Builtins.Globals.pas Outdated
Comment on lines +337 to +342
else if Descriptor is TGocciaPropertyDescriptorAccessor then
Result.DefineProperty(Keys[I],
TGocciaPropertyDescriptorAccessor.Create(
TGocciaPropertyDescriptorAccessor(Descriptor).Getter,
TGocciaPropertyDescriptorAccessor(Descriptor).Setter,
Descriptor.Flags));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Accessor properties should throw, not be copied.

Per the structured clone algorithm, accessor properties contain functions (getter/setter), and functions are non-serializable. Copying them directly violates the spec and could lead to unexpected behavior where cloned objects share function references with the original.

🐛 Proposed fix: throw DataCloneError for accessor properties
       if Descriptor is TGocciaPropertyDescriptorData then
       begin
         ClonedValue := StructuredCloneValue(TGocciaPropertyDescriptorData(Descriptor).Value, AMemory);
         Result.DefineProperty(Keys[I],
           TGocciaPropertyDescriptorData.Create(ClonedValue, Descriptor.Flags));
       end
-      else if Descriptor is TGocciaPropertyDescriptorAccessor then
-        Result.DefineProperty(Keys[I],
-          TGocciaPropertyDescriptorAccessor.Create(
-            TGocciaPropertyDescriptorAccessor(Descriptor).Getter,
-            TGocciaPropertyDescriptorAccessor(Descriptor).Setter,
-            Descriptor.Flags));
+      else if Descriptor is TGocciaPropertyDescriptorAccessor then
+        ThrowTypeError('Failed to execute ''structuredClone'': accessor property could not be cloned.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else if Descriptor is TGocciaPropertyDescriptorAccessor then
Result.DefineProperty(Keys[I],
TGocciaPropertyDescriptorAccessor.Create(
TGocciaPropertyDescriptorAccessor(Descriptor).Getter,
TGocciaPropertyDescriptorAccessor(Descriptor).Setter,
Descriptor.Flags));
else if Descriptor is TGocciaPropertyDescriptorAccessor then
ThrowTypeError('Failed to execute ''structuredClone'': accessor property could not be cloned.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 337 - 342, The code currently
copies accessor descriptors (TGocciaPropertyDescriptorAccessor) into the cloned
object (via Result.DefineProperty), but per the structured clone algorithm
accessor properties must cause a DataCloneError because their getter/setter
functions are non-serializable; change the branch that handles
TGocciaPropertyDescriptorAccessor (and any code path that would call
TGocciaPropertyDescriptorAccessor.Create) to raise/throw a DataCloneError
instead of defining the property (use the project's DataCloneError mechanism or
throw an appropriate EDataCloneError), referencing
TGocciaPropertyDescriptorAccessor, Descriptor, Keys[I], and
Result.DefineProperty to locate the code to modify.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 20, 2026

Benchmark Results

179 benchmarks · 🔴 1 regressed · 178 unchanged · avg -1.1%

arrays.js — 19 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 5,079 5,058 -0.4%
Array.from 10 elements 126,865 122,777 -3.2%
Array.of 10 elements 157,019 150,698 -4.0%
spread into new array 164,629 160,420 -2.6%
map over 50 elements 4,958 4,897 -1.2%
filter over 50 elements 4,635 4,535 -2.2%
reduce sum 50 elements 4,809 4,749 -1.2%
forEach over 50 elements 4,364 4,308 -1.3%
find in 50 elements 5,065 4,954 -2.2%
sort 20 elements 4,376 4,325 -1.2%
flat nested array 69,350 68,431 -1.3%
flatMap 37,256 37,087 -0.5%
map inside map (5x5) 10,069 10,034 -0.3%
filter inside map (5x10) 3,035 2,936 -3.3%
reduce inside map (5x10) 3,096 3,063 -1.1%
forEach inside forEach (5x10) 2,906 2,869 -1.3%
find inside some (10x10) 1,689 1,708 +1.1%
map+filter chain nested (5x20) 1,204 1,262 +4.8%
reduce flatten (10x5) 3,485 3,615 +3.7%
classes.js — 15 unchanged · avg -0.5%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 68,670 68,293 -0.5%
class with defaults 53,272 53,073 -0.4%
50 instances via Array.from 2,526 2,515 -0.4%
instance method call 33,214 32,855 -1.1%
static method call 55,599 55,236 -0.7%
single-level inheritance 27,388 27,178 -0.8%
two-level inheritance 24,945 24,663 -1.1%
private field access 34,950 34,991 +0.1%
private methods 40,571 40,399 -0.4%
getter/setter access 37,268 37,550 +0.8%
static getter read 69,366 68,124 -1.8%
static getter/setter pair 48,308 48,614 +0.6%
inherited static getter 41,255 41,379 +0.3%
inherited static setter 44,087 43,582 -1.1%
inherited static getter with this binding 34,814 34,665 -0.4%
closures.js — 11 unchanged · avg -2.7%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 58,117 57,473 -1.1%
closure over multiple variables 58,394 56,558 -3.1%
nested closures 63,227 61,134 -3.3%
function as argument 44,171 42,731 -3.3%
function returning function 58,492 56,558 -3.3%
compose two functions 35,807 34,685 -3.1%
fn.call 88,725 86,088 -3.0%
fn.apply 61,183 59,911 -2.1%
fn.bind 73,213 71,667 -2.1%
recursive sum to 50 5,060 4,971 -1.8%
recursive tree traversal 8,891 8,553 -3.8%
collections.js — 12 unchanged · avg -3.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 3,252 3,145 -3.3%
has lookup (50 elements) 5,594 5,385 -3.7%
delete elements 14,690 14,009 -4.6%
forEach iteration 3,265 3,139 -3.9%
spread to array 6,424 6,217 -3.2%
deduplicate array 24,487 24,202 -1.2%
set 50 entries 2,653 2,518 -5.1%
get lookup (50 entries) 3,195 3,025 -5.3%
has check 3,244 3,103 -4.3%
delete entries 8,246 7,771 -5.8%
forEach iteration 2,258 2,210 -2.1%
keys/values/entries 2,556 2,491 -2.5%
destructuring.js — 14 unchanged · avg -2.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 184,239 180,752 -1.9%
with rest element 135,478 131,509 -2.9%
with defaults 186,107 180,791 -2.9%
skip elements 197,905 193,783 -2.1%
nested array destructuring 104,795 101,249 -3.4%
swap variables 214,642 208,862 -2.7%
simple object destructuring 142,735 138,748 -2.8%
with defaults 171,013 168,579 -1.4%
with renaming 169,356 164,883 -2.6%
nested object destructuring 84,930 83,605 -1.6%
rest properties 89,074 87,782 -1.5%
object parameter 53,433 52,688 -1.4%
array parameter 66,750 65,202 -2.3%
mixed destructuring in map 6,218 6,234 +0.3%
fibonacci.js — 6 unchanged · avg -1.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 143 138 -3.3%
recursive fib(20) 13 12 -2.9%
iterative fib(20) via reduce 6,991 7,049 +0.8%
iterator fib(20) 3,920 3,975 +1.4%
iterator fib(20) via Iterator.from + take 4,123 4,040 -2.0%
iterator fib(20) last value via reduce 3,393 3,376 -0.5%
iterators.js — 20 unchanged · avg +1.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
Iterator.from({next}).toArray() — 20 elements 5,305 5,280 -0.5%
Iterator.from({next}).toArray() — 50 elements 2,259 2,296 +1.6%
spread pre-wrapped iterator — 20 elements 5,291 5,405 +2.1%
Iterator.from({next}).forEach — 50 elements 1,704 1,699 -0.3%
Iterator.from({next}).reduce — 50 elements 1,717 1,777 +3.5%
wrap array iterator 31,286 31,177 -0.3%
wrap plain {next()} object 3,668 3,644 -0.6%
map + toArray (50 elements) 1,470 1,509 +2.7%
filter + toArray (50 elements) 1,537 1,563 +1.7%
take(10) + toArray (50 element source) 8,420 8,647 +2.7%
drop(40) + toArray (50 element source) 2,172 2,256 +3.9%
chained map + filter + take (100 element source) 2,610 2,557 -2.0%
some + every (50 elements) 973 967 -0.6%
find (50 elements) 2,110 2,151 +1.9%
array.values().map().filter().toArray() 1,519 1,539 +1.3%
array.values().take(5).toArray() 7,255 7,441 +2.6%
array.values().drop(45).toArray() 4,620 4,676 +1.2%
map.entries() chained helpers 2,003 1,986 -0.9%
set.values() chained helpers 3,102 3,122 +0.6%
string iterator map + toArray 4,273 4,341 +1.6%
json.js — 20 unchanged · avg +2.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 115,928 116,542 +0.5%
parse nested object 74,077 75,355 +1.7%
parse array of objects 41,975 42,415 +1.0%
parse large flat object 36,020 37,415 +3.9%
parse mixed types 53,253 55,439 +4.1%
stringify simple object 100,013 102,038 +2.0%
stringify nested object 57,418 59,022 +2.8%
stringify array of objects 11,044 11,559 +4.7%
stringify mixed types 47,298 48,007 +1.5%
reviver doubles numbers 23,147 23,826 +2.9%
reviver filters properties 21,544 21,909 +1.7%
reviver on nested object 27,567 28,206 +2.3%
reviver on array 16,039 16,476 +2.7%
replacer function doubles numbers 22,040 22,770 +3.3%
replacer function excludes properties 28,864 29,763 +3.1%
array replacer (allowlist) 60,319 63,055 +4.5%
stringify with 2-space indent 56,296 58,275 +3.5%
stringify with tab indent 56,837 58,915 +3.7%
parse then stringify 35,822 36,456 +1.8%
stringify then parse 12,337 12,944 +4.9%
jsx.jsx — 21 unchanged · avg -0.7%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple element 117,803 116,694 -0.9%
self-closing element 119,185 119,769 +0.5%
element with string attribute 100,614 100,119 -0.5%
element with multiple attributes 87,790 87,773 -0.0%
element with expression attribute 90,925 91,825 +1.0%
text child 117,698 116,931 -0.7%
expression child 114,100 112,696 -1.2%
mixed text and expression 108,214 108,368 +0.1%
nested elements (3 levels) 45,930 45,575 -0.8%
sibling children 34,485 33,829 -1.9%
component element 84,957 85,254 +0.3%
component with children 53,865 51,940 -3.6%
dotted component 73,201 72,277 -1.3%
empty fragment 118,365 114,453 -3.3%
fragment with children 32,661 32,638 -0.1%
spread attributes 66,831 63,865 -4.4%
spread with overrides 54,982 56,358 +2.5%
shorthand props 86,250 88,808 +3.0%
nav bar structure 16,225 15,974 -1.5%
card component tree 19,652 19,066 -3.0%
10 list items via Array.from 8,089 8,179 +1.1%
numbers.js — 11 unchanged · avg -1.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 196,974 195,852 -0.6%
floating point arithmetic 220,161 220,618 +0.2%
number coercion 107,774 104,525 -3.0%
toFixed 73,419 72,432 -1.3%
toString 100,065 98,234 -1.8%
valueOf 130,350 127,760 -2.0%
toPrecision 91,700 92,978 +1.4%
Number.isNaN 160,572 156,814 -2.3%
Number.isFinite 154,652 150,482 -2.7%
Number.isInteger 160,533 154,117 -4.0%
Number.parseInt and parseFloat 148,019 145,295 -1.8%
objects.js — 7 unchanged · avg +0.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 234,973 233,477 -0.6%
create nested object 123,255 122,103 -0.9%
create 50 objects via Array.from 4,728 4,669 -1.2%
property read 104,160 108,271 +3.9%
Object.keys 74,467 77,270 +3.8%
Object.entries 48,445 47,561 -1.8%
spread operator 94,498 95,558 +1.1%
promises.js — 🔴 1 regressed, 11 unchanged · avg -4.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 310,844 295,364 -5.0%
new Promise(resolve => resolve(value)) 121,496 113,578 -6.5%
Promise.reject(reason) 325,918 302,680 🔴 -7.1%
resolve + then (1 handler) 106,634 101,367 -4.9%
resolve + then chain (3 deep) 43,828 42,326 -3.4%
resolve + then chain (10 deep) 14,816 14,321 -3.3%
reject + catch + then 64,393 61,173 -5.0%
resolve + finally + then 55,819 53,506 -4.1%
Promise.all (5 resolved) 22,643 21,451 -5.3%
Promise.race (5 resolved) 23,440 22,448 -4.2%
Promise.allSettled (5 mixed) 18,392 17,598 -4.3%
Promise.any (5 mixed) 22,411 21,558 -3.8%
strings.js — 11 unchanged · avg -4.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 286,975 280,844 -2.1%
template literal 259,092 256,819 -0.9%
string repeat 242,108 230,818 -4.7%
split and join 103,147 99,605 -3.4%
indexOf and includes 106,774 100,518 -5.9%
toUpperCase and toLowerCase 155,094 146,591 -5.5%
slice and substring 95,485 89,635 -6.1%
trim operations 114,939 107,693 -6.3%
replace and replaceAll 136,692 128,279 -6.2%
startsWith and endsWith 84,338 78,530 -6.9%
padStart and padEnd 123,633 117,421 -5.0%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

@frostney frostney changed the title Add structure clone Add stucturedClone Feb 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Around line 423-429: Replace the manual arity check in
TGocciaGlobals.StructuredCloneCallback (the AArgs.Length guard) with the shared
argument validator functions from Goccia.Arguments.Validator: call
RequireAtLeast or RequireExactly (whichever fits the API for structuredClone) at
the start of StructuredCloneCallback to enforce the expected argument count and
produce the standardized error message/behavior; ensure you import/use
Goccia.Arguments.Validator and remove the manual ThrowTypeError branch so the
validator handles the validation.
- Around line 129-130: The binding registers the 'structuredClone' name as a
hardcoded string in AScope.DefineLexicalBinding; replace that literal with the
centralized property-name constant (e.g. add/use a constant like
PropertyNames.StructuredClone in Goccia.Constants.PropertyNames) and use it when
calling AScope.DefineLexicalBinding for the TGocciaNativeFunctionValue created
with StructuredCloneCallback ('structuredClone', 1) so the binding name is
sourced from Goccia.Constants.PropertyNames instead of a raw string.
- Around line 320-389: CloneObject/CloneArray/CloneMap/CloneSet (and the
StructuredCloneValue flow) create new TGocciaValue instances that are only
referenced by locals and the AMemory dictionary during cloning and can be
collected by the GC mid-clone; fix by registering each newly created clone as a
temp root (call AddTempRoot) immediately after creating Result in CloneObject,
CloneArray, CloneMap and CloneSet and ensure you remove those temp roots
(RemoveTempRoot) after cloning completes—preferably in the surrounding
structured-clone call using a try/finally that iterates AMemory.Values to
RemoveTempRoot so all temp roots are cleaned up even on exceptions.

---

Duplicate comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Around line 323-339: GetOwnPropertyKeys returns a TStringList that is not
freed, causing a memory leak; wrap the call to AObj.GetOwnPropertyKeys in a
try..finally and free Keys after the loop (use Keys.Free or FreeAndNil) so Keys
is released even on exceptions—i.e. after obtaining Keys and before exiting the
method, ensure Keys is freed around the for I := 0 to Keys.Count - 1 ... end
block (referencing Keys, GetOwnPropertyKeys, and the loop handling
Descriptor/StructuredCloneValue).

Comment on lines +129 to +130
AScope.DefineLexicalBinding('structuredClone',
TGocciaNativeFunctionValue.Create(StructuredCloneCallback, 'structuredClone', 1), dtConst);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid hardcoded structuredClone binding name.

Use a property-name constant (add one if missing) so the global binding name is centralized and consistent. As per coding guidelines: Use runtime constants from split constant units instead of hardcoded string literals: Goccia.Constants.PropertyNames for property names, Goccia.Constants.TypeNames for type names, Goccia.Constants.ErrorNames for error names, Goccia.Constants.ConstructorNames for constructor names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 129 - 130, The binding
registers the 'structuredClone' name as a hardcoded string in
AScope.DefineLexicalBinding; replace that literal with the centralized
property-name constant (e.g. add/use a constant like
PropertyNames.StructuredClone in Goccia.Constants.PropertyNames) and use it when
calling AScope.DefineLexicalBinding for the TGocciaNativeFunctionValue created
with StructuredCloneCallback ('structuredClone', 1) so the binding name is
sourced from Goccia.Constants.PropertyNames instead of a raw string.

Comment on lines +320 to +389
Result := TGocciaObjectValue.Create;
AMemory.Add(AObj, Result);

Keys := AObj.GetOwnPropertyKeys;
for I := 0 to Keys.Count - 1 do
begin
Descriptor := AObj.GetOwnPropertyDescriptor(Keys[I]);
if not Assigned(Descriptor) then
Continue;

if Descriptor is TGocciaPropertyDescriptorData then
begin
ClonedValue := StructuredCloneValue(TGocciaPropertyDescriptorData(Descriptor).Value, AMemory);
Result.DefineProperty(Keys[I],
TGocciaPropertyDescriptorData.Create(ClonedValue, Descriptor.Flags));
end
else if Descriptor is TGocciaPropertyDescriptorAccessor then
ThrowTypeError('Failed to execute ''structuredClone'': accessor property ''' +
Keys[I] + ''' could not be cloned.');
end;
end;

function CloneArray(const AArr: TGocciaArrayValue;
const AMemory: TDictionary<TGocciaValue, TGocciaValue>): TGocciaArrayValue;
var
I: Integer;
Element: TGocciaValue;
begin
Result := TGocciaArrayValue.Create;
AMemory.Add(AArr, Result);

for I := 0 to AArr.Elements.Count - 1 do
begin
Element := AArr.Elements[I];
if Element = nil then
Result.Elements.Add(nil)
else
Result.Elements.Add(StructuredCloneValue(Element, AMemory));
end;
end;

function CloneMap(const AMap: TGocciaMapValue;
const AMemory: TDictionary<TGocciaValue, TGocciaValue>): TGocciaMapValue;
var
I: Integer;
Entry: TGocciaMapEntry;
begin
Result := TGocciaMapValue.Create;
AMemory.Add(AMap, Result);

for I := 0 to AMap.Entries.Count - 1 do
begin
Entry := AMap.Entries[I];
Result.SetEntry(
StructuredCloneValue(Entry.Key, AMemory),
StructuredCloneValue(Entry.Value, AMemory));
end;
end;

function CloneSet(const ASet: TGocciaSetValue;
const AMemory: TDictionary<TGocciaValue, TGocciaValue>): TGocciaSetValue;
var
I: Integer;
begin
Result := TGocciaSetValue.Create;
AMemory.Add(ASet, Result);

for I := 0 to ASet.Items.Count - 1 do
Result.AddItem(StructuredCloneValue(ASet.Items[I], AMemory));
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Temp-root cloned values while they live only in Memory.

CloneObject/CloneArray/CloneMap/CloneSet create new TGocciaValue instances that are only referenced by locals and the Memory dictionary until cloning completes. If GC runs mid-clone, these values can be collected. Add temp roots when creating clones and remove them after the callback returns (e.g., iterate Memory.Values in the finally). Based on learnings: Values held only by Pascal code (not in any GocciaScript scope) must be protected with AddTempRoot/RemoveTempRoot for the duration they are needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 320 - 389,
CloneObject/CloneArray/CloneMap/CloneSet (and the StructuredCloneValue flow)
create new TGocciaValue instances that are only referenced by locals and the
AMemory dictionary during cloning and can be collected by the GC mid-clone; fix
by registering each newly created clone as a temp root (call AddTempRoot)
immediately after creating Result in CloneObject, CloneArray, CloneMap and
CloneSet and ensure you remove those temp roots (RemoveTempRoot) after cloning
completes—preferably in the surrounding structured-clone call using a
try/finally that iterates AMemory.Values to RemoveTempRoot so all temp roots are
cleaned up even on exceptions.

Comment on lines +423 to +429
function TGocciaGlobals.StructuredCloneCallback(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
var
Memory: TDictionary<TGocciaValue, TGocciaValue>;
begin
if AArgs.Length = 0 then
ThrowTypeError('Failed to execute ''structuredClone'': 1 argument required, but only 0 present.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the shared argument validator for the arity check.

Replace the manual AArgs.Length guard with RequireAtLeast/RequireExactly from Goccia.Arguments.Validator for consistent behavior and error messages. As per coding guidelines: Use Goccia.Arguments.Validator.pas for standardized argument count/type validation with RequireExactly and RequireAtLeast instead of manual validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 423 - 429, Replace the manual
arity check in TGocciaGlobals.StructuredCloneCallback (the AArgs.Length guard)
with the shared argument validator functions from Goccia.Arguments.Validator:
call RequireAtLeast or RequireExactly (whichever fits the API for
structuredClone) at the start of StructuredCloneCallback to enforce the expected
argument count and produce the standardized error message/behavior; ensure you
import/use Goccia.Arguments.Validator and remove the manual ThrowTypeError
branch so the validator handles the validation.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Around line 344-391: In CloneArray, CloneMap, and CloneSet register each newly
created clone with AddTempRoot immediately after creating Result (e.g., after
Result := TGocciaArrayValue.Create / TGocciaMapValue.Create /
TGocciaSetValue.Create) so they won’t be collected during cloning, and ensure
these temp roots are removed in the finally block of StructuredCloneCallback;
update StructuredCloneCallback to clean up the AddTempRoot registrations for
these clones when cloning completes or on error.
- Around line 131-132: Replace the hardcoded binding name 'structuredClone' in
the AScope.DefineLexicalBinding call with the relevant constant from
Goccia.Constants.PropertyNames (e.g., PROP_STRUCTURED_CLONE); modify the call
that currently reads AScope.DefineLexicalBinding('structuredClone',
TGocciaNativeFunctionValue.Create(StructuredCloneCallback, 'structuredClone',
1), dtConst) so both occurrences of the literal are replaced by the constant,
and add the appropriate unit to the uses clause if required so
PROP_STRUCTURED_CLONE is in scope.
- Around line 325-341: GetOwnPropertyKeys returns a TStringList (Keys) that is
not freed, causing a memory leak; wrap the usage of Keys in a try..finally and
call Keys.Free in the finally block (around the loop that inspects Descriptor,
calls StructuredCloneValue, and calls Result.DefineProperty), leaving the
existing logic for TGocciaPropertyDescriptorData and
TGocciaPropertyDescriptorAccessor unchanged so Keys is always freed even on
exceptions.
- Around line 429-430: Replace the manual arity guard in the structuredClone
implementation (the AArgs.Length = 0 check that calls ThrowTypeError) with the
shared validator: call RequireAtLeast(AArgs, 1, 'structuredClone') from
Goccia.Arguments.Validator at the start of the structuredClone function and
remove the existing AArgs.Length check and ThrowTypeError call so the common
validator handles the argument validation and error messaging.

@frostney frostney merged commit 30692da into main Feb 21, 2026
4 checks passed
@frostney frostney deleted the feat-structured-clone branch February 21, 2026 11:32
@coderabbitai coderabbitai Bot mentioned this pull request Feb 22, 2026
@frostney frostney added the new feature New feature or request label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant