Conversation
Introduce TGCManagedObject (GarbageCollector.Managed.pas) as the shared base class for all GC-managed objects, providing GCMarked flag and virtual MarkReferences. TGocciaValue, TGocciaScope, and TSouffleHeapObject all inherit from TGCManagedObject. Add TGenericGarbageCollector (GarbageCollector.Generic.pas) with the core mark-and-sweep algorithm: managed object list, pinned objects, temp roots, external root marker, and threshold-based collection. Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
- TGocciaValue extends TGCManagedObject (removes FGCMarked field, inherits MarkReferences from base class) - TGocciaScope extends TGCManagedObject (removes FGCMarked field, overrides MarkReferences to mark scope chain and bindings) - TSouffleHeapObject extends TGCManagedObject (removes FGCMarked field, overrides MarkReferences to mark delegate) Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
- TGocciaGarbageCollector extends TGenericGarbageCollector, adds scope management (root scopes, active scope stack, managed scopes) - TSouffleGarbageCollector extends TGenericGarbageCollector, preserves AllocateObject API and destructor cleanup of managed heap objects - Backend destructor unchanged (Souffle GC collection only) Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
- AGENTS.md: update component table with new GC units, update GC awareness section and design patterns - docs/architecture.md: rewrite Memory Management section with generic GC hierarchy diagram and unit table Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
Remove TSouffleGarbageCollector entirely. Both the interpreter (TGocciaValue, TGocciaScope) and the Souffle VM (TSouffleHeapObject) now share a single TGenericGarbageCollector singleton. The Souffle VM accesses it via TGenericGarbageCollector.Instance (no Goccia.* import needed). Key changes: - TGenericGarbageCollector gains singleton pattern (Instance, Shutdown) and AllocateObject convenience method - TGocciaGarbageCollector.Initialize creates the singleton stored in the base class - Support multiple external root markers (AddExternalRootMarker/ RemoveExternalRootMarker) so both the VM and runtime can register markers on the shared GC - Disable automatic collection during VM execution to prevent sweeping Souffle objects on the Pascal stack - BenchmarkRunner explicitly collects between files to reclaim memory (critical for 32-bit Windows OOM prevention) - Delete souffle/Souffle.GarbageCollector.pas Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
|
Cursor Agent can help with this pull request. Just |
📝 Walkthrough🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…or for everything Fold root object tracking (AddRootObject/RemoveRootObject) and active root stack (PushActiveRoot/PopActiveRoot) into TGenericGarbageCollector. Add Initialize class method. Remove FreeAndNil/SysUtils dependency. Scopes now register via RegisterObject into the same managed pool as values and Souffle heap objects. Delete Goccia.GarbageCollector.pas. Rename all callers across 40+ files: RegisterValue→RegisterObject, PinValue→PinObject, AddRoot→AddRootObject, RemoveRoot→RemoveRootObject, PushActiveScope→PushActiveRoot, PopActiveScope→PopActiveRoot, RegisterScope→RegisterObject. Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
Temp-root the accumulator objects (AllTestResults, AllFailedTests) and call GC.Collect after processing each file's results. This reclaims unreachable values between files, preventing memory accumulation on 32-bit Windows. Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
Benchmark Results254 benchmarks Interpreted: 🟢 112 improved · 🔴 10 regressed · 132 unchanged · avg +6.5% arraybuffer.js — Interp: 🔴 2, 12 unch. · avg -1.1% · Bytecode: 🟢 2, 🔴 4, 8 unch. · avg -2.5%
arrays.js — Interp: 🟢 6, 13 unch. · avg +5.0% · Bytecode: 🟢 17, 2 unch. · avg +22.7%
async-await.js — Interp: 6 unch. · avg +2.5% · Bytecode: 🟢 1, 🔴 1, 4 unch. · avg +0.3%
classes.js — Interp: 🟢 4, 27 unch. · avg +3.4% · Bytecode: 🟢 15, 16 unch. · avg +7.8%
closures.js — Interp: 🟢 2, 9 unch. · avg +3.5% · Bytecode: 🟢 11 · avg +51.1%
collections.js — Interp: 🟢 4, 🔴 6, 2 unch. · avg -2.6% · Bytecode: 🟢 2, 🔴 5, 5 unch. · avg -21.8%
destructuring.js — Interp: 🟢 9, 13 unch. · avg +5.2% · Bytecode: 🟢 22 · avg +41.7%
fibonacci.js — Interp: 🟢 6, 2 unch. · avg +10.0% · Bytecode: 🟢 7, 🔴 1 · avg +56.0%
for-of.js — Interp: 🟢 7 · avg +18.5% · Bytecode: 🟢 1, 🆕 6 · avg +27.4%
iterators.js — Interp: 🟢 17, 3 unch. · avg +12.8% · Bytecode: 🟢 18, 2 unch. · avg +24.1%
json.js — Interp: 🟢 11, 9 unch. · avg +8.0% · Bytecode: 🆕 20
jsx.jsx — Interp: 🟢 14, 7 unch. · avg +9.3% · Bytecode: 🆕 21
numbers.js — Interp: 🟢 1, 10 unch. · avg +0.3% · Bytecode: 🆕 11
objects.js — Interp: 🟢 5, 2 unch. · avg +9.7% · Bytecode: 🆕 7
promises.js — Interp: 🟢 6, 🔴 1, 5 unch. · avg +6.7% · Bytecode: 🆕 12
strings.js — Interp: 🟢 5, 6 unch. · avg +7.1% · Bytecode: 🆕 11
typed-arrays.js — Interp: 🟢 15, 🔴 1, 6 unch. · avg +12.1% · Bytecode: 🆕 22
Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant. |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
units/Goccia.Builtins.TestAssertions.pas (1)
1072-1104:⚠️ Potential issue | 🟠 MajorThe returned expectation still doesn't keep the unwrapped value alive.
The temp root only covers
DrainQueue. After returningTGocciaExpectationValue.Create(Promise.PromiseResult, ...), the resolved/rejected value is referenced only fromFActualValue, butTGocciaExpectationValuedoes not mark that field. A later collection can reclaim it before the chained matcher runs.As per coding guidelines: `Each value type must override MarkReferences to mark all TGocciaValue references it holds (prototype, closure, elements, property values, etc.).`Suggested fix
TGocciaExpectationValue = class(TGocciaObjectValue) @@ function GetResolves(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function GetRejects(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; + procedure MarkReferences; override; end;+procedure TGocciaExpectationValue.MarkReferences; +begin + if GCMarked then Exit; + inherited; + if Assigned(FActualValue) then + FActualValue.MarkReferences; +end; + function TGocciaExpectationValue.GetResolves(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;Also applies to: 1112-1144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Builtins.TestAssertions.pas` around lines 1072 - 1104, The returned TGocciaExpectationValue is not keeping the unwrapped Promise result alive; instead of relying on a temp root around DrainQueue, implement/override MarkReferences in TGocciaExpectationValue to mark the internal TGocciaValue fields (the stored unwrapped value passed to TGocciaExpectationValue.Create, plus any other TGocciaValue fields like FTestAssertions or prototype/closure fields) so the GC will not reclaim the wrapped value before chained matchers run; update TGocciaExpectationValue.MarkReferences to call Mark on that stored value (and call inherited where appropriate) rather than trying to extend the temp-root scope in the resolves code path.units/Goccia.Evaluator.pas (1)
243-252:⚠️ Potential issue | 🟠 MajorGuard these
TGarbageCollector.Instancecalls consistently.The rest of this file still treats
Instanceas nullable, but these paths dereference it unconditionally. If evaluation can happen before GC initialization or during shutdown, they now crash instead of failing as JS code.Also applies to: 742-761, 1425-1447, 1930-2240, 2521-2561, 3022-3056
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Evaluator.pas` around lines 243 - 252, The code unconditionally dereferences TGarbageCollector.Instance (calling AddTempRoot/RemoveTempRoot) which can be nil; change to grab a local variable (e.g. gc := TGarbageCollector.Instance) and call gc.AddTempRoot(Iterator) only if Assigned(gc), then in the finally block call gc.RemoveTempRoot(Iterator) only if Assigned(gc) so you don't crash when GC is uninitialized or during shutdown; apply the same Assigned-guard pattern to all other occurrences mentioned (around lines handling Iterator/AdvanceNext and the other ranges) so AddTempRoot/RemoveTempRoot are always paired only when the GC was actually added.units/Goccia.MicrotaskQueue.pas (1)
149-161:⚠️ Potential issue | 🟠 MajorRelease the queue-owned temp root after a task is consumed.
queueMicrotaskroots the callback before enqueueing it, butDrainQueueonly removes the extra root it adds for execution. AfterFQueue.Clear, the original queue-owned root is still live, so processed callbacks never become collectible.Based on learnings, values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.MicrotaskQueue.pas` around lines 149 - 161, DrainQueue currently removes only the extra execution roots but leaves the original queue-owned temp root alive after FQueue.Clear, preventing collected callbacks from being freed; update DrainQueue so that after clearing FQueue it also calls TGarbageCollector.Instance.RemoveTempRoot for the temp root that was added by queueMicrotask (the queue-owned root), ensuring any Task.Handler/Task.Value/Promise roots held solely by the Pascal queue are released; reference queueMicrotask (where the AddTempRoot is applied), DrainQueue, FQueue.Clear and TGarbageCollector.Instance.RemoveTempRoot when making the change.units/Goccia.Values.TemporalPlainDate.pas (1)
452-461:⚠️ Potential issue | 🟠 MajorGuard
TGarbageCollector.Instancehere.This is now an unguarded singleton dereference in a codebase that otherwise treats
Instanceas nullable. IfDateSubtractruns before GC initialization or during teardown, this path will raise an access violation.Suggested fix
function TGocciaTemporalPlainDateValue.DateSubtract(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; var D: TGocciaTemporalPlainDateValue; Dur: TGocciaTemporalDurationValue; Arg: TGocciaValue; ObjArg: TGocciaObjectValue; DurRec: TTemporalDurationRecord; NegatedDur: TGocciaTemporalDurationValue; NewArgs: TGocciaArgumentsCollection; + GarbageCollector: TGarbageCollector; begin + GarbageCollector := TGarbageCollector.Instance; D := AsPlainDate(AThisValue, 'PlainDate.prototype.subtract'); Arg := AArgs.GetElement(0); @@ - TGarbageCollector.Instance.AddTempRoot(NegatedDur); + if Assigned(GarbageCollector) then + GarbageCollector.AddTempRoot(NegatedDur); try NewArgs := TGocciaArgumentsCollection.Create([NegatedDur]); try Result := DateAdd(NewArgs, AThisValue); finally NewArgs.Free; end; finally - TGarbageCollector.Instance.RemoveTempRoot(NegatedDur); + if Assigned(GarbageCollector) then + GarbageCollector.RemoveTempRoot(NegatedDur); end; end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Values.TemporalPlainDate.pas` around lines 452 - 461, The TGarbageCollector.Instance dereference is unguarded (in the DateSubtract/DateAdd path using NegatedDur), so wrap the AddTempRoot/RemoveTempRoot calls with a nil-check: test Assigned(TGarbageCollector.Instance) before calling AddTempRoot and record a boolean (e.g., GCAdded) to indicate you added the temp root, then in the finally only call RemoveTempRoot if GCAdded is true; keep the NewArgs creation and DateAdd call intact (refer to TGarbageCollector.Instance, AddTempRoot, RemoveTempRoot, NewArgs and DateAdd) so you avoid an access violation when Instance is nil during init/teardown.units/Goccia.Builtins.GlobalArray.pas (1)
245-267:⚠️ Potential issue | 🟠 MajorTemp-root
ResultObjin everyArray.fromconstruction path.Lines 245-267 only protect the iterator branch. The array, string, and array-like branches still keep
ResultObjin a Pascal local while mapping callbacks or per-element allocations run; the string fast path is especially exposed because it allocates a newTGocciaStringLiteralValuebefore storing it. Non-iterator inputs can still lose the partially-built result under the unified collector.As per coding guidelines, "Values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.GlobalArray.pas` around lines 245 - 267, The partially-built ResultObj is only temp-rooted in the iterator branch; protect ResultObj in all Array.from code paths (string fast path, array-like, and standard array branches) by calling TGarbageCollector.Instance.AddTempRoot(ResultObj) immediately after creating it and ensuring a matching RemoveTempRoot(ResultObj) in the enclosing finally block so mapping callbacks (MapCallback/MapArgs), per-element allocations (e.g., TGocciaStringLiteralValue) and CreateDataProperty calls cannot be collected; mirror the iterator branch's temp-root placement and removal around any code that invokes InvokeCallable or allocates element values.units/Goccia.Builtins.Benchmark.pas (2)
432-493:⚠️ Potential issue | 🟠 Major
SingleResultand the finalResultObjstill need temp roots while they are being populated.Rooting
ResultsArrayis not enough. EachTGocciaObjectValue.Createin Lines 445 and 476 lives only in a Pascal local untilSetElement/Result :=runs, and theAssignProperty(...Create(...))calls in between can allocate enough to sweep those objects first.As per coding guidelines, "Values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.Benchmark.pas` around lines 432 - 493, The code roots ResultsArray but not the transient TGocciaObjectValue instances (SingleResult and ResultObj) created while populating it, risking GC during AssignProperty calls; after each TGocciaObjectValue.Create for SingleResult (the ones created in the try and except blocks) call TGarbageCollector.Instance.AddTempRoot(SingleResult) before doing AssignProperty/SetElement and then call TGarbageCollector.Instance.RemoveTempRoot(SingleResult) immediately after ResultsArray.SetElement; likewise, after creating ResultObj call AddTempRoot(ResultObj) before AssignProperty calls and RemoveTempRoot(ResultObj) just before returning Result (and ensure all Add/Remove use TGarbageCollector.Instance and mirror each other in the finally block).
280-295:⚠️ Potential issue | 🟠 MajorRoot
SetupResultbeforeDrainQueue.Line 289 returns a value that only lives in
SetupResult, but Line 290 can drain arbitrary microtasks before Line 295 roots it. That leaves setup payloads collectible before warmup/calibration captures them.Suggested fix
StartNanoseconds := GetNanoseconds; SetupResult := ABenchCase.SetupFunction.Call(EmptyArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + if Assigned(SetupResult) and Assigned(GC) then + GC.AddTempRoot(SetupResult); if Assigned(TGocciaMicrotaskQueue.Instance) then TGocciaMicrotaskQueue.Instance.DrainQueue; Result.SetupMs := (GetNanoseconds - StartNanoseconds) / 1000000; - - if Assigned(SetupResult) and Assigned(GC) then - GC.AddTempRoot(SetupResult);As per coding guidelines, "Values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.Benchmark.pas` around lines 280 - 295, The SetupResult returned by ABenchCase.SetupFunction.Call can be collected during TGocciaMicrotaskQueue.Instance.DrainQueue because it isn’t rooted yet; immediately after calling ABenchCase.SetupFunction.Call store and protect the returned value by calling GC.AddTempRoot(SetupResult) (using GC := TGarbageCollector.Instance) before invoking TGocciaMicrotaskQueue.Instance.DrainQueue, and keep that temp root until after warmup/calibration when it is explicitly removed with RemoveTempRoot; reference SetupResult, ABenchCase.SetupFunction.Call, TGocciaMicrotaskQueue.Instance.DrainQueue, and TGarbageCollector.AddTempRoot to locate and adjust the code.units/Goccia.Engine.pas (1)
252-273:⚠️ Potential issue | 🟠 MajorPin the shared
nullsingleton too.
PinSingletonsnow routes throughPinObject, but it still never pinsTGocciaNullLiteralValue.NullValue. Under the unified collector that leaves one of the language singletons collectible even though the runtime treats it as immortal.Suggested fix
PinIfAssigned(TGocciaUndefinedLiteralValue.UndefinedValue); + PinIfAssigned(TGocciaNullLiteralValue.NullValue); PinIfAssigned(TGocciaBooleanLiteralValue.TrueValue);As per coding guidelines, "Singleton values (
undefined,null,true,false,NaN,Infinity) are pinned viaTGocciaGarbageCollector.Instance.PinValueduring engine initialization inPinSingletons."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Engine.pas` around lines 252 - 273, Add pinning for the null singleton in TGocciaEngine.PinSingletons: locate the PinIfAssigned call sequence in TGocciaEngine.PinSingletons and add a call for TGocciaNullLiteralValue.NullValue (e.g., PinIfAssigned(TGocciaNullLiteralValue.NullValue)) so the null singleton is pinned by the garbage collector just like UndefinedValue, TrueValue, FalseValue, etc.; use the existing PinIfAssigned helper (which delegates to TGarbageCollector.Instance.PinObject) to keep the pattern consistent.units/Goccia.Values.ClassValue.pas (1)
747-757:⚠️ Potential issue | 🟠 MajorKeep
Instancetemp-rooted through the native-init path too.Right now the temp root only covers
ConstructorToCall.Call. Built-ins that skip a user constructor and go throughInitializeNativeFromArgumentsat Lines 756-757 leave the freshly created object reachable only from Pascal locals during initialization, which is exactly the lifetime window the temp-root rule is meant to protect.Proposed fix
- if Assigned(ConstructorToCall) then - begin - TGarbageCollector.Instance.AddTempRoot(Instance); - try - ConstructorToCall.Call(AArguments, Instance); - finally - TGarbageCollector.Instance.RemoveTempRoot(Instance); - end; - end - else if Assigned(NativeInstance) and (NativeInstance is TGocciaInstanceValue) then - TGocciaInstanceValue(NativeInstance).InitializeNativeFromArguments(AArguments); - - Result := Instance; + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.AddTempRoot(Instance); + try + if Assigned(ConstructorToCall) then + ConstructorToCall.Call(AArguments, Instance) + else if Assigned(NativeInstance) and (NativeInstance is TGocciaInstanceValue) then + TGocciaInstanceValue(NativeInstance).InitializeNativeFromArguments(AArguments); + + Result := Instance; + finally + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.RemoveTempRoot(Instance); + end;As per coding guidelines: Values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.Values.ClassValue.pas` around lines 747 - 757, The newly created object Instance is temp-rooted only when ConstructorToCall is present but not when the native-init path is taken; wrap the call to TGocciaInstanceValue(NativeInstance).InitializeNativeFromArguments(AArguments) with TGarbageCollector.Instance.AddTempRoot(Instance) before the call and TGarbageCollector.Instance.RemoveTempRoot(Instance) in a finally block so Instance is protected during InitializeNativeFromArguments; use the same AddTempRoot/RemoveTempRoot pattern as around ConstructorToCall.Call and refer to ConstructorToCall, NativeInstance, TGocciaInstanceValue.InitializeNativeFromArguments and TGarbageCollector.Instance.AddTempRoot/RemoveTempRoot.TestRunner.dpr (1)
337-401:⚠️ Potential issue | 🟠 MajorUse
finallyfor both temp-root cleanup and per-file collection.This block currently has two GC-lifetime holes: an exception between
AddTempRootandRemoveTempRootleaks permanent roots, and theContinueon Line 369 skips the new per-fileGC.Collect.Suggested fix
if Assigned(GC) then begin GC.AddTempRoot(AllTestResults); GC.AddTempRoot(AllFailedTests); end; - - AllTestResults.AssignProperty('totalTests', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('totalRunTests', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('passed', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('failed', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('skipped', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('assertions', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('duration', TGocciaNumberLiteralValue.ZeroValue); - AllTestResults.AssignProperty('failedTests', AllFailedTests); + try + AllTestResults.AssignProperty('totalTests', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('totalRunTests', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('passed', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('failed', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('skipped', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('assertions', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('duration', TGocciaNumberLiteralValue.ZeroValue); + AllTestResults.AssignProperty('failedTests', AllFailedTests); - for I := 0 to AFiles.Count - 1 do - begin - ... - FileResult := RunScriptFromFile(AFiles[I]); - if FileResult.TestResult = nil then - Continue; - ... - if Assigned(GC) then - GC.Collect; - end; + for I := 0 to AFiles.Count - 1 do + begin + try + ... + FileResult := RunScriptFromFile(AFiles[I]); + if FileResult.TestResult = nil then + Continue; + ... + finally + if Assigned(GC) then + GC.Collect; + end; + end; - if Assigned(GC) then - begin - GC.RemoveTempRoot(AllTestResults); - GC.RemoveTempRoot(AllFailedTests); - end; + finally + if Assigned(GC) then + begin + GC.RemoveTempRoot(AllTestResults); + GC.RemoveTempRoot(AllFailedTests); + end; + end;As per coding guidelines, values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor the duration they are needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestRunner.dpr` around lines 337 - 401, Wrap the lifetime management of AllTestResults and AllFailedTests in a try..finally so GC.RemoveTempRoot is always called (move GC.AddTempRoot(AllTestResults)/GC.AddTempRoot(AllFailedTests) before a try and call GC.RemoveTempRoot(...) in the finally), and ensure the per-file GC.Collect call in the loop cannot be skipped by the Continue (either call GC.Collect before using Continue or restructure the per-file processing so GC.Collect is executed in a small try..finally around RunScriptFromFile/FileResult handling); update the block referencing AllTestResults, AllFailedTests, GC.AddTempRoot/GC.RemoveTempRoot, the for I := 0 to AFiles.Count - 1 loop, RunScriptFromFile and the existing Continue so temp roots are always removed and per-file GC.Collect always runs.units/Goccia.Values.IteratorValue.pas (1)
284-295:⚠️ Potential issue | 🔴 CriticalRoot the callback inputs too, not just the iterator/accumulator.
These loops now protect
Iterator/Accumulator, but the currentValue(and, for the same reason,Callback) is still only held in Pascal locals when you re-enter user code viaInvokeIteratorCallback(...)/Call(...). A GC triggered inside that callback can collect the current element out from under the native helper.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 neededSuggested pattern
Callback := AArgs.GetElement(0); + TGarbageCollector.Instance.AddTempRoot(Callback); Index := 0; TGarbageCollector.Instance.AddTempRoot(Iterator); try Value := Iterator.DirectNext(Done); while not Done do begin - InvokeIteratorCallback(Callback, Value, Index); + TGarbageCollector.Instance.AddTempRoot(Value); + try + InvokeIteratorCallback(Callback, Value, Index); + finally + TGarbageCollector.Instance.RemoveTempRoot(Value); + end; Inc(Index); Value := Iterator.DirectNext(Done); end; finally TGarbageCollector.Instance.RemoveTempRoot(Iterator); + TGarbageCollector.Instance.RemoveTempRoot(Callback); end;Apply the same pattern anywhere this unit passes a locally-held
TGocciaValueback into user code (reduce,some,every,find).Also applies to: 317-350, 400-488
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Values.IteratorValue.pas` around lines 284 - 295, The loop roots the Iterator but not the TGocciaValue instances passed into user code or the Callback reference, so ensure any Value and Callback that are passed to InvokeIteratorCallback (and similar methods like reduce/some/every/find) are protected: call TGarbageCollector.Instance.AddTempRoot(Value) and AddTempRoot(Callback) before InvokeIteratorCallback/Call and remove them in a matching finally via RemoveTempRoot(Value) and RemoveTempRoot(Callback); mirror this pattern around every place in this unit where a locally-held TGocciaValue is handed back to user code (e.g., the sections noted around reduce, some, every, find and the other ranges mentioned).ScriptLoader.dpr (1)
205-217:⚠️ Potential issue | 🔴 CriticalPer-file GC shutdown breaks prototype singleton cache—object pointers will be freed but reused in next cycle.
EmitBytecodeandEmitWasmnow initialize and tear down the garbage collector after each file. However, class-var prototype caches (e.g.,FSharedArrayPrototype,FSharedMapPrototype,FPrototypeMethodHost) are never reset tonilbetween cycles.On first file,
InitializePrototypeassignsFSharedXxxPrototype := TGocciaObjectValue.Create;and the new object is pinned in GC1. After shutdown, GC1 and all its pinned objects are freed.On second file, a new GC2 is created, but the class var still holds the freed pointer.
InitializePrototypehas an early exit (if Assigned(FSharedArrayPrototype) then Exit;), so it never rebuilds—the method returns a dead object reference or the pointer is used in a different GC context, causing corruption or crash.Either reset all class-var caches to
nilinTGarbageCollector.Shutdown, or move GC lifetime outside the per-file loop to a single cycle per process.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ScriptLoader.dpr` around lines 205 - 217, The per-file GC shutdown leaves class-var prototype caches pointing at freed objects; update TGarbageCollector.Shutdown to nil-out the prototype/cache class variables (e.g., FSharedArrayPrototype, FSharedMapPrototype, FPrototypeMethodHost and any other FShared* prototypes) so InitializePrototype’s guarded early-exit (if Assigned(...)) will rebuild prototypes under the new GC, or alternatively move GC lifetime out of EmitBytecode/EmitWasm to a single process-wide initialization; pick the nil-reset approach and ensure Shutdown clears all cached class vars before freeing GC state.units/Goccia.Runtime.Operations.pas (2)
7925-7975:⚠️ Potential issue | 🟠 MajorRelease
EnumObjonce the wrapper owns it.
EnumObjis temp-rooted beforeWrapGocciaValue, but never unrooted afterward. Every finalized enum will stay pinned in the temp-root set even when nothing in JS can reach it anymore.As per coding guidelines: Values held only by Pascal code (not in any GocciaScript scope) must be protected with `AddTempRoot`/`RemoveTempRoot` for the duration they are needed.Suggested fix
- if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.AddTempRoot(EnumObj); - - Result := WrapGocciaValue(EnumObj); + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.AddTempRoot(EnumObj); + try + Result := WrapGocciaValue(EnumObj); + finally + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.RemoveTempRoot(EnumObj); + end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Runtime.Operations.pas` around lines 7925 - 7975, The function FinalizeEnum adds EnumObj to the GC temp roots via TGarbageCollector.Instance.AddTempRoot but never removes it, leaking temp roots; after successfully wrapping EnumObj with WrapGocciaValue (result of WrapGocciaValue(EnumObj) assigned to Result) call TGarbageCollector.Instance.RemoveTempRoot(EnumObj) (or remove it in a finally block after wrapping) so the temporary root is released once ownership is transferred to the wrapper; ensure you still RemoveTempRoot on all success paths and do not remove it on exception paths where ownership wasn't transferred.
3585-3721:⚠️ Potential issue | 🟠 MajorBalance the iterator temp-roots before returning.
The new
AddTempRootcalls in this method never get paired withRemoveTempRootafterWrapGocciaValuetakes ownership. That pins every bridged iterator for the lifetime of the runtime and will accumulate across long benchmark/test runs.Suggested fix
function TGocciaRuntimeOperations.GetIterator( const AIterable: TSouffleValue; const ATryAsync: Boolean): TSouffleValue; var GocciaVal: TGocciaValue; Iterator: TGocciaIteratorValue; IteratorMethod, IteratorObj, NextMethod: TGocciaValue; CallArgs: TGocciaArgumentsCollection; + GC: TGarbageCollector; begin + GC := TGarbageCollector.Instance; {$IFDEF BRIDGE_METRICS} Inc(GBridgeMetrics.GetIteratorCount); {$ENDIF} try ... if GocciaVal is TGocciaMapValue then begin Iterator := TGocciaMapIteratorValue.Create(GocciaVal, mkEntries); - if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.AddTempRoot(Iterator); - Result := WrapGocciaValue(Iterator); + if Assigned(GC) then + GC.AddTempRoot(Iterator); + try + Result := WrapGocciaValue(Iterator); + finally + if Assigned(GC) then + GC.RemoveTempRoot(Iterator); + end; Exit; end; ... - if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.AddTempRoot(Iterator); - Result := WrapGocciaValue(Iterator); + if Assigned(GC) then + GC.AddTempRoot(Iterator); + try + Result := WrapGocciaValue(Iterator); + finally + if Assigned(GC) then + GC.RemoveTempRoot(Iterator); + end;Apply the same pattern to the
IteratorObjasync path as well.
As per coding guidelines: Values held only by Pascal code (not in any GocciaScript scope) must be protected withAddTempRoot/RemoveTempRootfor 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.Runtime.Operations.pas` around lines 3585 - 3721, GetIterator is adding temporary GC roots (TGarbageCollector.Instance.AddTempRoot) for Iterator and IteratorObj but never removing them after ownership transfers via WrapGocciaValue, leaking pinned iterators; fix by pairing each AddTempRoot with a RemoveTempRoot immediately after WrapGocciaValue (or use a try/finally that calls RemoveTempRoot in the finally) so the temp root is cleared once Result is produced, and apply the same pattern in both the async IteratorObj path and the synchronous Iterator paths where AddTempRoot is called; reference TGocciaRuntimeOperations.GetIterator, WrapGocciaValue, AddTempRoot and RemoveTempRoot, and the local variables Iterator and IteratorObj to locate the spots to update.docs/souffle-vm.md (1)
695-705:⚠️ Potential issue | 🟡 MinorUpdate the remaining split-GC references in this doc.
This section now says Souffle and the interpreter share one collector, but Line 551 still says “Souffle GC” and Lines 942-966 still describe “Two GC systems” / “dual GC tracking.” Please bring those passages in line with the new model so contributors do not get contradictory guidance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/souffle-vm.md` around lines 695 - 705, Update the outdated split-GC wording to reflect the unified collector TGarbageCollector.Instance: replace any “Souffle GC” and any “Two GC systems” / “dual GC tracking” descriptions with the single-shared GC model, remove references to separate interpreter/VM collectors, and clarify that MarkVMRoots and MarkExternalRoots are the external-root hooks used to cover VM registers, call stack and bridge call paths; also update the CollectIfNeeded/threshold note to state it’s currently disabled until MarkExternalRoots covers all bridge paths and ensure references to TSouffleHeapObject, PinObject/UnpinObject, AddTempRoot/RemoveTempRoot and O(1) membership remain consistent with the unified singleton design.
🧹 Nitpick comments (3)
docs/adding-built-in-types.md (1)
75-82: Keep the starter template free of direct GC dependencies.This snippet never calls
TGarbageCollector, so addingGarbageCollector.Generichere teaches contributors to copy an unnecessary dependency into every new value unit. If a type later needs temp roots or explicit GC calls, that unit can be added then.Based on learnings,
TGocciaSharedPrototype.Createautomatically pins both prototype and method host.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/adding-built-in-types.md` around lines 75 - 82, The uses clause incorrectly includes GarbageCollector.Generic even though this unit does not call TGarbageCollector; remove GarbageCollector.Generic from the uses list (alongside the existing Goccia.* and Goccia.Values.* units) so the starter template has no direct GC dependency, and do not add any TGarbageCollector calls—rely on TGocciaSharedPrototype.Create which already pins the prototype and method host.units/GarbageCollector.Managed.pas (1)
8-13: Rename this base type before it spreads further.
TGCManagedObjectbecomes part of the new core GC surface, so theGCabbreviation is worth fixing now rather than after more units inherit from it.As per coding guidelines, use full words in class, function, method, and type names instead of abbreviations (e.g.,
TGocciaGarbageCollectornotTGocciaGC).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/GarbageCollector.Managed.pas` around lines 8 - 13, Rename the abbreviated base type and its members to use full words: change class TGCManagedObject to TGarbageCollectedObject, rename the private field FGCMarked to FGarbageCollectedMarked, and rename the property GCMarked to GarbageCollectedMarked (keep MarkReferences as-is and preserve virtual). Update all references/usages, typecasts and unit/interface declarations that mention TGCManagedObject, FGCMarked or GCMarked throughout the codebase to the new identifiers so compilation and behavior remain identical.docs/souffle-vm.md (1)
704-704: The membership type is now too narrow for the unified collector.
TDictionary<TSouffleHeapObject, Boolean>still reads like a Souffle-only implementation detail. With the generic collector managing a shared managed-object base, this should describe the generic managed type instead ofTSouffleHeapObject.✏️ Suggested doc tweak
-- **O(1) membership** — Pinned objects and temp roots use `TDictionary<TSouffleHeapObject, Boolean>` for hash-set semantics +- **O(1) membership** — Pinned objects and temp roots use a dictionary-backed set keyed by the generic managed-object base for O(1) membership checks🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/souffle-vm.md` at line 704, Update the documentation phrase that currently refers to the Souffle-specific type TDictionary<TSouffleHeapObject, Boolean> so it describes the generic managed-object membership type used by the unified collector; replace the Souffle-specific symbol with a generic descriptor (e.g., TDictionary<TManagedObject, Boolean> or "dictionary of managed objects") and mention that pinned objects and temp roots use this generic managed-object membership structure to provide O(1) membership semantics for the unified collector.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AGENTS.md`:
- Line 159: Update the AGENTS.md table row for the GocciaScript Backend
(TGocciaSouffleBackend) to remove the outdated lifecycle claims: delete the
phrases "collects on destruction" and "shuts down in finalization" and instead
state the current behavior (automatic GC collection is disabled and
TGocciaSouffleBackend does not perform explicit collection or shutdown). Make
the wording concise and accurate so the row now reads that the backend
initializes TGarbageCollector with automatic collection disabled and leaves
collection/shutdown to external management.
In `@BenchmarkRunner.dpr`:
- Around line 184-186: Move the per-file garbage collection into a finally block
so it runs even if the file benchmark raises an exception: in both
CollectBenchmarkFileInterpreted and CollectBenchmarkFileBytecode wrap the
existing body that calls PopulateFileResult(FileResult, ScriptResult, AReporter,
Result) (and any subsequent success-only logic) in a try..finally and place the
check and call to TGarbageCollector.Instance.Collect inside the finally (i.e. if
Assigned(TGarbageCollector.Instance) then TGarbageCollector.Instance.Collect),
applying the same change for the other occurrence noted.
In `@docs/code-style.md`:
- Around line 417-418: The example shows manual calls to
TGarbageCollector.Instance.PinObject for FSharedPrototype and
FPrototypeMethodHost which are redundant and encourage double-pinning; remove
those two PinObject lines from the example and rely on
TGocciaSharedPrototype.Create (which already pins the prototype and method host)
so the example demonstrates correct single-pinning behavior for
TGocciaSharedPrototype.Create, avoiding references to
TGarbageCollector.Instance.PinObject for FSharedPrototype and
FPrototypeMethodHost.
In `@docs/souffle-vm.md`:
- Line 1010: The docs contain a contradiction: the section intro states "all
Souffle VM source files live in `souffle/` and use `Souffle.` prefix naming" but
the table includes `GarbageCollector.Generic.pas` which doesn't follow that
convention; fix by either moving the `GarbageCollector.Generic.pas` row into a
new or existing shared-infrastructure subsection (e.g., "Shared/Interpreter
Infrastructure") or adjust the section intro to say that most files follow the
`souffle/` and `Souffle.` convention while some shared components like
`GarbageCollector.Generic.pas` live outside that namespace; update the table row
or the intro text accordingly so both match.
In `@docs/testing.md`:
- Around line 527-529: The documentation claims GC.Collect runs "after each
file", but TestRunner.dpr skips calling GC.Collect on the single-file execution
path; either update docs to state GC.Collect runs only when multiple files are
executed or modify TestRunner.dpr to invoke GC.Collect on the single-file branch
as well. Locate the single-file execution branch in TestRunner.dpr (the code
path that handles running a lone test file) and add the same explicit GC.Collect
call used in the multi-file loop, or alternatively update docs/testing.md to
clarify that GC.Collect only runs between files when running multiple files.
In `@docs/value-system.md`:
- Around line 118-120: The documented sample should be updated to reflect the
unified GC by showing that FGCMarked and AfterConstruction now live on
TGCManagedObject (not on TGocciaValue) and that classes like TGocciaValue
inherit from TGCManagedObject rather than TInterfacedObject; change the sample
to declare TGCManagedObject with FGCMarked and an overridden AfterConstruction
that registers with TGarbageCollector.Instance, then show TGocciaValue
inheriting from TGCManagedObject and keep the descriptions of MarkReferences and
RuntimeCopy and examples (TGocciaObjectValue, TGocciaFunctionValue,
TGocciaArrayValue) unchanged so the prose and code sample no longer contradict
each other.
In `@souffle/Souffle.VM.pas`:
- Around line 96-104: Do not cache the GC singleton in FGC because it can be
freed before TSouffleVM.Destroy; instead, call TGarbageCollector.Instance at use
sites and guard that result. Replace uses of the cached FGC in the
constructor/initializer and in TSouffleVM.Destroy with a local GC :=
TGarbageCollector.Instance (or similar direct Instance call) and only call
AddExternalRootMarker/RemoveExternalRootMarker(MarkVMRoots) when that local GC
is Assigned, ensuring you never dereference a potentially dangling FGC field.
In `@units/GarbageCollector.Generic.pas`:
- Around line 140-144: TGarbageCollector.UnregisterObject currently only removes
AObject from FManagedObjects; update TGarbageCollector.UnregisterObject to also
remove any references to AObject from FPinnedObjects, FTempRoots, FRootObjects
and any entries in FActiveRootStack so no dangling pointers remain. Locate the
UnregisterObject method and for each container (FPinnedObjects, FTempRoots,
FRootObjects) call the appropriate Remove/Contains+Delete routine to remove
AObject, and for FActiveRootStack iterate its entries and remove or null-out
entries equal to AObject (or pop/update the stack) so active-root
scanning/MarkRoots will not dereference a freed object. Ensure you use the
existing collection APIs and equality semantics used elsewhere in the class
(e.g., same Remove/IndexOf pattern as FManagedObjects) to keep behavior
consistent.
- Around line 115-123: The destructor TGarbageCollector.Destroy currently frees
tracking containers (FManagedObjects, FPinnedObjects, FTempRoots, FRootObjects,
FActiveRootStack) but because FManagedObjects was created with OwnsObjects =
False it leaves the managed instances allocated; before freeing FManagedObjects
iterate its entries and free/release each managed object (the actual
deallocation method used elsewhere in this unit — e.g., Obj.Free or
Obj.Release/Dispose — consistent with how managed objects are normally
destroyed), then clear the list and continue to free
FPinnedObjects/FTempRoots/FRootObjects/FActiveRootStack; ensure the loop is
nil-safe and wrapped in try..finally so container cleanup always runs even if a
free raises.
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 8689-8691: The current loop marks keys/values from
FArrayBridgeReverse, effectively making the reverse cache a permanent root;
instead, either prune/clear FArrayBridgeReverse whenever FArrayBridgeCache is
cleared or stop rooting entries from the reverse cache here. Update the code in
TGocciaRuntimeOperations to (a) remove the MarkReferences calls over
FArrayBridgeReverse (do not treat BridgeKey or its value as GC roots), or (b)
add logic to prune matching entries from FArrayBridgeReverse whenever
FArrayBridgeCache is cleared/evicted so the reverse map does not hold immortal
references; apply the same change at the other occurrence (around lines
8745–8747) and reference FArrayBridgeCache, FArrayBridgeReverse, BridgeKey and
TSouffleHeapObject.MarkReferences when making the change.
- Around line 1934-1937: The GC-managed helper objects TGocciaBridgedFunction
and TGocciaSuperCallHelper must override MarkReferences to mark their wrapped
TGocciaValue fields (FGocciaFn and FSuperClass) so the unified GC doesn't
reclaim those Goccia values; add/implement MarkReferences in
TGocciaBridgedFunction.MarkReferences and TGocciaSuperCallHelper.MarkReferences
that calls inherited and then invokes the garbage‑collector's marking routine on
FGocciaFn / FSuperClass (e.g., TGarbageCollector.Instance.MarkValue or the
project’s equivalent mark method) so these TGocciaValue references are traced;
apply the same pattern to the other helper/value types mentioned (the other
occurrences you noted).
---
Outside diff comments:
In `@docs/souffle-vm.md`:
- Around line 695-705: Update the outdated split-GC wording to reflect the
unified collector TGarbageCollector.Instance: replace any “Souffle GC” and any
“Two GC systems” / “dual GC tracking” descriptions with the single-shared GC
model, remove references to separate interpreter/VM collectors, and clarify that
MarkVMRoots and MarkExternalRoots are the external-root hooks used to cover VM
registers, call stack and bridge call paths; also update the
CollectIfNeeded/threshold note to state it’s currently disabled until
MarkExternalRoots covers all bridge paths and ensure references to
TSouffleHeapObject, PinObject/UnpinObject, AddTempRoot/RemoveTempRoot and O(1)
membership remain consistent with the unified singleton design.
In `@ScriptLoader.dpr`:
- Around line 205-217: The per-file GC shutdown leaves class-var prototype
caches pointing at freed objects; update TGarbageCollector.Shutdown to nil-out
the prototype/cache class variables (e.g., FSharedArrayPrototype,
FSharedMapPrototype, FPrototypeMethodHost and any other FShared* prototypes) so
InitializePrototype’s guarded early-exit (if Assigned(...)) will rebuild
prototypes under the new GC, or alternatively move GC lifetime out of
EmitBytecode/EmitWasm to a single process-wide initialization; pick the
nil-reset approach and ensure Shutdown clears all cached class vars before
freeing GC state.
In `@TestRunner.dpr`:
- Around line 337-401: Wrap the lifetime management of AllTestResults and
AllFailedTests in a try..finally so GC.RemoveTempRoot is always called (move
GC.AddTempRoot(AllTestResults)/GC.AddTempRoot(AllFailedTests) before a try and
call GC.RemoveTempRoot(...) in the finally), and ensure the per-file GC.Collect
call in the loop cannot be skipped by the Continue (either call GC.Collect
before using Continue or restructure the per-file processing so GC.Collect is
executed in a small try..finally around RunScriptFromFile/FileResult handling);
update the block referencing AllTestResults, AllFailedTests,
GC.AddTempRoot/GC.RemoveTempRoot, the for I := 0 to AFiles.Count - 1 loop,
RunScriptFromFile and the existing Continue so temp roots are always removed and
per-file GC.Collect always runs.
In `@units/Goccia.Builtins.Benchmark.pas`:
- Around line 432-493: The code roots ResultsArray but not the transient
TGocciaObjectValue instances (SingleResult and ResultObj) created while
populating it, risking GC during AssignProperty calls; after each
TGocciaObjectValue.Create for SingleResult (the ones created in the try and
except blocks) call TGarbageCollector.Instance.AddTempRoot(SingleResult) before
doing AssignProperty/SetElement and then call
TGarbageCollector.Instance.RemoveTempRoot(SingleResult) immediately after
ResultsArray.SetElement; likewise, after creating ResultObj call
AddTempRoot(ResultObj) before AssignProperty calls and RemoveTempRoot(ResultObj)
just before returning Result (and ensure all Add/Remove use
TGarbageCollector.Instance and mirror each other in the finally block).
- Around line 280-295: The SetupResult returned by ABenchCase.SetupFunction.Call
can be collected during TGocciaMicrotaskQueue.Instance.DrainQueue because it
isn’t rooted yet; immediately after calling ABenchCase.SetupFunction.Call store
and protect the returned value by calling GC.AddTempRoot(SetupResult) (using GC
:= TGarbageCollector.Instance) before invoking
TGocciaMicrotaskQueue.Instance.DrainQueue, and keep that temp root until after
warmup/calibration when it is explicitly removed with RemoveTempRoot; reference
SetupResult, ABenchCase.SetupFunction.Call,
TGocciaMicrotaskQueue.Instance.DrainQueue, and TGarbageCollector.AddTempRoot to
locate and adjust the code.
In `@units/Goccia.Builtins.GlobalArray.pas`:
- Around line 245-267: The partially-built ResultObj is only temp-rooted in the
iterator branch; protect ResultObj in all Array.from code paths (string fast
path, array-like, and standard array branches) by calling
TGarbageCollector.Instance.AddTempRoot(ResultObj) immediately after creating it
and ensuring a matching RemoveTempRoot(ResultObj) in the enclosing finally block
so mapping callbacks (MapCallback/MapArgs), per-element allocations (e.g.,
TGocciaStringLiteralValue) and CreateDataProperty calls cannot be collected;
mirror the iterator branch's temp-root placement and removal around any code
that invokes InvokeCallable or allocates element values.
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 1072-1104: The returned TGocciaExpectationValue is not keeping the
unwrapped Promise result alive; instead of relying on a temp root around
DrainQueue, implement/override MarkReferences in TGocciaExpectationValue to mark
the internal TGocciaValue fields (the stored unwrapped value passed to
TGocciaExpectationValue.Create, plus any other TGocciaValue fields like
FTestAssertions or prototype/closure fields) so the GC will not reclaim the
wrapped value before chained matchers run; update
TGocciaExpectationValue.MarkReferences to call Mark on that stored value (and
call inherited where appropriate) rather than trying to extend the temp-root
scope in the resolves code path.
In `@units/Goccia.Engine.pas`:
- Around line 252-273: Add pinning for the null singleton in
TGocciaEngine.PinSingletons: locate the PinIfAssigned call sequence in
TGocciaEngine.PinSingletons and add a call for TGocciaNullLiteralValue.NullValue
(e.g., PinIfAssigned(TGocciaNullLiteralValue.NullValue)) so the null singleton
is pinned by the garbage collector just like UndefinedValue, TrueValue,
FalseValue, etc.; use the existing PinIfAssigned helper (which delegates to
TGarbageCollector.Instance.PinObject) to keep the pattern consistent.
In `@units/Goccia.Evaluator.pas`:
- Around line 243-252: The code unconditionally dereferences
TGarbageCollector.Instance (calling AddTempRoot/RemoveTempRoot) which can be
nil; change to grab a local variable (e.g. gc := TGarbageCollector.Instance) and
call gc.AddTempRoot(Iterator) only if Assigned(gc), then in the finally block
call gc.RemoveTempRoot(Iterator) only if Assigned(gc) so you don't crash when GC
is uninitialized or during shutdown; apply the same Assigned-guard pattern to
all other occurrences mentioned (around lines handling Iterator/AdvanceNext and
the other ranges) so AddTempRoot/RemoveTempRoot are always paired only when the
GC was actually added.
In `@units/Goccia.MicrotaskQueue.pas`:
- Around line 149-161: DrainQueue currently removes only the extra execution
roots but leaves the original queue-owned temp root alive after FQueue.Clear,
preventing collected callbacks from being freed; update DrainQueue so that after
clearing FQueue it also calls TGarbageCollector.Instance.RemoveTempRoot for the
temp root that was added by queueMicrotask (the queue-owned root), ensuring any
Task.Handler/Task.Value/Promise roots held solely by the Pascal queue are
released; reference queueMicrotask (where the AddTempRoot is applied),
DrainQueue, FQueue.Clear and TGarbageCollector.Instance.RemoveTempRoot when
making the change.
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 7925-7975: The function FinalizeEnum adds EnumObj to the GC temp
roots via TGarbageCollector.Instance.AddTempRoot but never removes it, leaking
temp roots; after successfully wrapping EnumObj with WrapGocciaValue (result of
WrapGocciaValue(EnumObj) assigned to Result) call
TGarbageCollector.Instance.RemoveTempRoot(EnumObj) (or remove it in a finally
block after wrapping) so the temporary root is released once ownership is
transferred to the wrapper; ensure you still RemoveTempRoot on all success paths
and do not remove it on exception paths where ownership wasn't transferred.
- Around line 3585-3721: GetIterator is adding temporary GC roots
(TGarbageCollector.Instance.AddTempRoot) for Iterator and IteratorObj but never
removing them after ownership transfers via WrapGocciaValue, leaking pinned
iterators; fix by pairing each AddTempRoot with a RemoveTempRoot immediately
after WrapGocciaValue (or use a try/finally that calls RemoveTempRoot in the
finally) so the temp root is cleared once Result is produced, and apply the same
pattern in both the async IteratorObj path and the synchronous Iterator paths
where AddTempRoot is called; reference TGocciaRuntimeOperations.GetIterator,
WrapGocciaValue, AddTempRoot and RemoveTempRoot, and the local variables
Iterator and IteratorObj to locate the spots to update.
In `@units/Goccia.Values.ClassValue.pas`:
- Around line 747-757: The newly created object Instance is temp-rooted only
when ConstructorToCall is present but not when the native-init path is taken;
wrap the call to
TGocciaInstanceValue(NativeInstance).InitializeNativeFromArguments(AArguments)
with TGarbageCollector.Instance.AddTempRoot(Instance) before the call and
TGarbageCollector.Instance.RemoveTempRoot(Instance) in a finally block so
Instance is protected during InitializeNativeFromArguments; use the same
AddTempRoot/RemoveTempRoot pattern as around ConstructorToCall.Call and refer to
ConstructorToCall, NativeInstance,
TGocciaInstanceValue.InitializeNativeFromArguments and
TGarbageCollector.Instance.AddTempRoot/RemoveTempRoot.
In `@units/Goccia.Values.IteratorValue.pas`:
- Around line 284-295: The loop roots the Iterator but not the TGocciaValue
instances passed into user code or the Callback reference, so ensure any Value
and Callback that are passed to InvokeIteratorCallback (and similar methods like
reduce/some/every/find) are protected: call
TGarbageCollector.Instance.AddTempRoot(Value) and AddTempRoot(Callback) before
InvokeIteratorCallback/Call and remove them in a matching finally via
RemoveTempRoot(Value) and RemoveTempRoot(Callback); mirror this pattern around
every place in this unit where a locally-held TGocciaValue is handed back to
user code (e.g., the sections noted around reduce, some, every, find and the
other ranges mentioned).
In `@units/Goccia.Values.TemporalPlainDate.pas`:
- Around line 452-461: The TGarbageCollector.Instance dereference is unguarded
(in the DateSubtract/DateAdd path using NegatedDur), so wrap the
AddTempRoot/RemoveTempRoot calls with a nil-check: test
Assigned(TGarbageCollector.Instance) before calling AddTempRoot and record a
boolean (e.g., GCAdded) to indicate you added the temp root, then in the finally
only call RemoveTempRoot if GCAdded is true; keep the NewArgs creation and
DateAdd call intact (refer to TGarbageCollector.Instance, AddTempRoot,
RemoveTempRoot, NewArgs and DateAdd) so you avoid an access violation when
Instance is nil during init/teardown.
---
Nitpick comments:
In `@docs/adding-built-in-types.md`:
- Around line 75-82: The uses clause incorrectly includes
GarbageCollector.Generic even though this unit does not call TGarbageCollector;
remove GarbageCollector.Generic from the uses list (alongside the existing
Goccia.* and Goccia.Values.* units) so the starter template has no direct GC
dependency, and do not add any TGarbageCollector calls—rely on
TGocciaSharedPrototype.Create which already pins the prototype and method host.
In `@docs/souffle-vm.md`:
- Line 704: Update the documentation phrase that currently refers to the
Souffle-specific type TDictionary<TSouffleHeapObject, Boolean> so it describes
the generic managed-object membership type used by the unified collector;
replace the Souffle-specific symbol with a generic descriptor (e.g.,
TDictionary<TManagedObject, Boolean> or "dictionary of managed objects") and
mention that pinned objects and temp roots use this generic managed-object
membership structure to provide O(1) membership semantics for the unified
collector.
In `@units/GarbageCollector.Managed.pas`:
- Around line 8-13: Rename the abbreviated base type and its members to use full
words: change class TGCManagedObject to TGarbageCollectedObject, rename the
private field FGCMarked to FGarbageCollectedMarked, and rename the property
GCMarked to GarbageCollectedMarked (keep MarkReferences as-is and preserve
virtual). Update all references/usages, typecasts and unit/interface
declarations that mention TGCManagedObject, FGCMarked or GCMarked throughout the
codebase to the new identifiers so compilation and behavior remain identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 873dd5d1-a6c9-4589-aad1-375ff7cb5f05
📒 Files selected for processing (61)
AGENTS.mdBenchmarkRunner.dprREADME.mdScriptLoader.dprTestRunner.dprdocs/adding-built-in-types.mddocs/architecture.mddocs/benchmarks.mddocs/code-style.mddocs/design-decisions.mddocs/embedding.mddocs/souffle-vm.mddocs/testing.mddocs/value-system.mdsouffle/Souffle.GarbageCollector.passouffle/Souffle.Heap.passouffle/Souffle.VM.NativeFunction.passouffle/Souffle.VM.passouffle/Souffle.Value.pasunits/GarbageCollector.Generic.pasunits/GarbageCollector.Managed.pasunits/Goccia.AST.Expressions.pasunits/Goccia.Builtins.Base.pasunits/Goccia.Builtins.Benchmark.pasunits/Goccia.Builtins.GlobalArray.pasunits/Goccia.Builtins.GlobalPromise.pasunits/Goccia.Builtins.Globals.pasunits/Goccia.Builtins.Math.pasunits/Goccia.Builtins.Temporal.pasunits/Goccia.Builtins.TestAssertions.pasunits/Goccia.Compiler.Test.pasunits/Goccia.Engine.Backend.pasunits/Goccia.Engine.pasunits/Goccia.Evaluator.pasunits/Goccia.GarbageCollector.pasunits/Goccia.Interpreter.pasunits/Goccia.MicrotaskQueue.pasunits/Goccia.Runtime.Operations.pasunits/Goccia.Scope.pasunits/Goccia.SharedPrototype.pasunits/Goccia.Values.ArrayBufferValue.pasunits/Goccia.Values.ArrayValue.pasunits/Goccia.Values.AsyncFunctionValue.pasunits/Goccia.Values.BooleanObjectValue.pasunits/Goccia.Values.ClassValue.pasunits/Goccia.Values.FunctionBase.pasunits/Goccia.Values.FunctionValue.pasunits/Goccia.Values.Iterator.Lazy.pasunits/Goccia.Values.IteratorValue.pasunits/Goccia.Values.MapValue.pasunits/Goccia.Values.NumberObjectValue.pasunits/Goccia.Values.ObjectValue.pasunits/Goccia.Values.Primitives.pasunits/Goccia.Values.PromiseValue.pasunits/Goccia.Values.SetValue.pasunits/Goccia.Values.SharedArrayBufferValue.pasunits/Goccia.Values.StringObjectValue.pasunits/Goccia.Values.SymbolValue.pasunits/Goccia.Values.TemporalPlainDate.pasunits/Goccia.Values.ToPrimitive.pasunits/Goccia.Values.TypedArrayValue.pas
💤 Files with no reviewable changes (2)
- souffle/Souffle.GarbageCollector.pas
- units/Goccia.GarbageCollector.pas
- Move BenchmarkRunner GC.Collect to finally blocks so collection runs even when a benchmark file throws (both interpreted and bytecode paths) - Add MarkReferences for TGocciaBridgedFunction.FGocciaFn and TGocciaSuperCallHelper.FSuperClass to prevent premature collection - Update AGENTS.md backend row to reflect actual GC lifecycle - Fix docs/code-style.md shared prototype example to use TGocciaSharedPrototype.Create (auto-pins, no manual PinObject needed) - Move GarbageCollector.Generic.pas to shared infrastructure section in docs/souffle-vm.md (not a Souffle.* prefixed file) - Fix docs/value-system.md sample to show TGCManagedObject base class - Clarify docs/testing.md: GC.Collect runs between files in multi-file mode Made-with: Cursor
Thanks for the benchmark results. The interpreted mode improvements (+5.8% avg) are a nice side effect of the GC unification. The bytecode regressions in collections.js (Map get/has/delete, keys/values/entries) appear to be pre-existing measurement noise unrelated to the GC changes — these operations don't interact with bridge caches or GC collection. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/souffle-vm.md (1)
702-702:⚠️ Potential issue | 🟡 MinorIncorrect method name
MarkVMRoots.The documentation references a
MarkVMRootsmethod that doesn't exist in theTGarbageCollectorAPI. The actual mechanism usesAddExternalRootMarkerto register a callback. The protectedMarkRootsmethod (shown in GarbageCollector.Generic.pas:230-249) iterates over registered external root markers.📝 Proposed correction
-- **External root marker** — The VM registers `MarkVMRoots` to mark all values in the register file and call stack during collection +- **External root marker** — The VM registers a root marker callback via `AddExternalRootMarker` to mark all values in the register file and call stack during collection🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/souffle-vm.md` at line 702, The docs incorrectly reference a non-existent MarkVMRoots method; update the text to describe the actual API: explain that callers register a callback via AddExternalRootMarker and that TGarbageCollector.MarkRoots (the protected routine that iterates registered external root markers) will invoke those callbacks during collection; replace the MarkVMRoots mention with AddExternalRootMarker and a brief note that the registered callback is invoked by TGarbageCollector.MarkRoots.units/Goccia.Runtime.Operations.pas (1)
5271-5285:⚠️ Potential issue | 🔴 CriticalTemp-root
NewArrwhile callbacks are running.After
AllocateObject(NewArr), the array is only held in a Pascal local until the method returns.VM.ExecuteFunction(...)can trigger GC, soNewArrcan be swept mid-loop inmap,filter, andflatMap.Suggested fix
NewArr := TSouffleArray.Create(Arr.Count); NewArr.Delegate := GNativeArrayJoinRuntime.VM.ArrayDelegate; if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.AllocateObject(NewArr); - for I := 0 to Arr.Count - 1 do - begin - if SouffleIsHole(Arr.Get(I)) then - begin - NewArr.Push(SouffleNilWithFlags(GOCCIA_NIL_HOLE)); - Continue; - end; - NewArr.Push(GNativeArrayJoinRuntime.VM.ExecuteFunction(Callback, - [SouffleNil, Arr.Get(I), SouffleInteger(I), AReceiver])); - end; + begin + TGarbageCollector.Instance.AllocateObject(NewArr); + TGarbageCollector.Instance.AddTempRoot(NewArr); + end; + try + for I := 0 to Arr.Count - 1 do + begin + if SouffleIsHole(Arr.Get(I)) then + begin + NewArr.Push(SouffleNilWithFlags(GOCCIA_NIL_HOLE)); + Continue; + end; + NewArr.Push(GNativeArrayJoinRuntime.VM.ExecuteFunction(Callback, + [SouffleNil, Arr.Get(I), SouffleInteger(I), AReceiver])); + end; + finally + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.RemoveTempRoot(NewArr); + end; Result := SouffleReference(NewArr);Use the same pattern for
NativeArrayFilterandNativeArrayFlatMap.As per coding guidelines: "Values held only by Pascal code (not in any GocciaScript scope) must be protected with AddTempRoot/RemoveTempRoot for the duration they are needed".
Also applies to: 5303-5317, 5696-5718
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Runtime.Operations.pas` around lines 5271 - 5285, NewArr is only a Pascal-local after AllocateObject and can be collected if GNativeArrayJoinRuntime.VM.ExecuteFunction triggers GC; wrap NewArr with the temp-root API by calling AddTempRoot(NewArr) immediately after TGarbageCollector.Instance.AllocateObject(NewArr) and calling RemoveTempRoot(NewArr) after the loop (before returning Result) so the array cannot be swept mid-iteration; apply the same pattern in NativeArrayFilter and NativeArrayFlatMap (and the other indicated ranges) around their AllocateObject / loops that call VM.ExecuteFunction.
♻️ Duplicate comments (1)
units/Goccia.Runtime.Operations.pas (1)
8694-8696:⚠️ Potential issue | 🟠 MajorDon't make
FArrayBridgeReversea permanent root set.
FArrayBridgeReverseis never pruned whenFArrayBridgeCacheis cleared. Marking both its values and keys here keeps every bridged array pair alive for the lifetime ofTGocciaRuntimeOperations, which can bring back the cross-file memory growth this PR is trying to remove.Also applies to: 8750-8752
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Runtime.Operations.pas` around lines 8694 - 8696, The loop is marking entries in FArrayBridgeReverse (both values and keys) which effectively makes FArrayBridgeReverse a permanent root; stop marking from FArrayBridgeReverse. Instead, iterate only the current FArrayBridgeCache entries and call MarkReferences on their TSouffleHeapObject values (or otherwise mark only objects reachable from FArrayBridgeCache). Update the code paths in TGocciaRuntimeOperations that call TSouffleHeapObject(…).MarkReferences so they reference FArrayBridgeCache items rather than FArrayBridgeReverse (apply the same change for the other occurrence around the second block that mirrored this behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/souffle-vm.md`:
- Line 698: The doc incorrectly states that BenchmarkRunner calls Collect before
each measurement round; update the sentence in docs/souffle-vm.md to reflect the
actual behavior: BenchmarkRunner calls Collect in the finally block after each
file completes (see BenchmarkRunner.dpr where Collect is invoked at the end of
file processing), not before measurement rounds; revise the "Lifecycle"
paragraph to remove or replace the phrase "The BenchmarkRunner additionally
calls `Collect` before each measurement round" with wording that it calls
`Collect` after each file completes to reclaim memory between script executions.
In `@docs/value-system.md`:
- Around line 110-113: The sample exposes the private backing field FGCMarked
instead of the public property GCMarked; update the declaration for
TGCManagedObject to show GCMarked (the public property callers use) rather than
FGCMarked, and keep MarkReferences as-is; ensure the prose and example reference
GCMarked to match units/GarbageCollector.Managed.pas and align with the
documentation on Line 122.
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 3631-3642: The AddTempRoot calls around iterator creation (e.g.,
when creating TGocciaMapIteratorValue/TGocciaSetIteratorValue in GetIterator and
similar sites) must be balanced by calling
TGarbageCollector.Instance.RemoveTempRoot immediately after WrapGocciaValue
returns so the temp root only guards the value during wrapping; update each
AddTempRoot in this block (and the other mentioned blocks and EnumObj) to add
the temp root, call Result := WrapGocciaValue(Iterator), then remove the temp
root (guard for Assigned(TGarbageCollector.Instance) when removing) so
iterators/enums are not permanently rooted.
---
Outside diff comments:
In `@docs/souffle-vm.md`:
- Line 702: The docs incorrectly reference a non-existent MarkVMRoots method;
update the text to describe the actual API: explain that callers register a
callback via AddExternalRootMarker and that TGarbageCollector.MarkRoots (the
protected routine that iterates registered external root markers) will invoke
those callbacks during collection; replace the MarkVMRoots mention with
AddExternalRootMarker and a brief note that the registered callback is invoked
by TGarbageCollector.MarkRoots.
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 5271-5285: NewArr is only a Pascal-local after AllocateObject and
can be collected if GNativeArrayJoinRuntime.VM.ExecuteFunction triggers GC; wrap
NewArr with the temp-root API by calling AddTempRoot(NewArr) immediately after
TGarbageCollector.Instance.AllocateObject(NewArr) and calling
RemoveTempRoot(NewArr) after the loop (before returning Result) so the array
cannot be swept mid-iteration; apply the same pattern in NativeArrayFilter and
NativeArrayFlatMap (and the other indicated ranges) around their AllocateObject
/ loops that call VM.ExecuteFunction.
---
Duplicate comments:
In `@units/Goccia.Runtime.Operations.pas`:
- Around line 8694-8696: The loop is marking entries in FArrayBridgeReverse
(both values and keys) which effectively makes FArrayBridgeReverse a permanent
root; stop marking from FArrayBridgeReverse. Instead, iterate only the current
FArrayBridgeCache entries and call MarkReferences on their TSouffleHeapObject
values (or otherwise mark only objects reachable from FArrayBridgeCache). Update
the code paths in TGocciaRuntimeOperations that call
TSouffleHeapObject(…).MarkReferences so they reference FArrayBridgeCache items
rather than FArrayBridgeReverse (apply the same change for the other occurrence
around the second block that mirrored this behavior).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e0da2ca0-6337-4daf-8aca-8a5a7a107524
📒 Files selected for processing (7)
AGENTS.mdBenchmarkRunner.dprdocs/code-style.mddocs/souffle-vm.mddocs/testing.mddocs/value-system.mdunits/Goccia.Runtime.Operations.pas
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
units/Goccia.Builtins.Benchmark.pas (1)
289-295:⚠️ Potential issue | 🟠 MajorRoot
SetupResultbefore draining microtasks.
SetupResultis only held in a Pascal local until Line 295.DrainQueueon Lines 290-291 can re-enter script code before that root is added, so the setup value has a collection window right afterCallreturns.Suggested fix
StartNanoseconds := GetNanoseconds; SetupResult := ABenchCase.SetupFunction.Call(EmptyArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + if Assigned(SetupResult) and Assigned(GC) then + GC.AddTempRoot(SetupResult); if Assigned(TGocciaMicrotaskQueue.Instance) then TGocciaMicrotaskQueue.Instance.DrainQueue; Result.SetupMs := (GetNanoseconds - StartNanoseconds) / 1000000; - - if Assigned(SetupResult) and Assigned(GC) then - GC.AddTempRoot(SetupResult);As per coding guidelines, "Values held only by Pascal code (not in any GocciaScript scope) must be protected with
AddTempRoot/RemoveTempRootfor 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.Benchmark.pas` around lines 289 - 295, SetupResult returned from ABenchCase.SetupFunction.Call is vulnerable to collection because TGocciaMicrotaskQueue.Instance.DrainQueue can re-enter script code before the GC root is added; protect SetupResult immediately after the Call by calling GC.AddTempRoot(SetupResult) before invoking TGocciaMicrotaskQueue.Instance.DrainQueue and ensure you call GC.RemoveTempRoot(SetupResult) after you no longer need it (e.g., after computing Result.SetupMs and any subsequent use), so the root is held for the full duration SetupResult is only referenced by Pascal code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AGENTS.md`:
- Line 367: The example is confusing because it shows "TGarbageCollector not
TGenericGC" where "TGenericGC" is not a reasonable abbreviation; update the
example to use a realistic abbreviation by replacing "TGenericGC" with "TGC" so
it reads "TGarbageCollector not TGC" (leave the exceptions list like `AST`,
`JSON`, `REPL`, `ISO`, `Utils` unchanged).
In `@docs/architecture.md`:
- Around line 356-360: The table documents the wrong active-scope API names;
update references of PushActiveRoot/PopActiveRoot to the correct
PushActiveScope/PopActiveScope, and adjust the explanatory text to state that
scopes register with the GC in their constructor and are pushed/popped via
PushActiveScope/PopActiveScope (used from TGocciaFunctionValue.Call), while
keeping mention of AddRootObject/RemoveRootObject and temp/external roots
intact; ensure the class names TGCManagedObject and TGarbageCollector and the
methods Initialize/Instance/Shutdown remain unchanged.
In `@units/Goccia.Builtins.Benchmark.pas`:
- Around line 428-430: The ResultsArray local must be created before entering
the try/finally to avoid passing an uninitialized variable to the GC on
exception; move the TGocciaArrayValue.Create call so ResultsArray is assigned
before the try, then inside the try/finally only call
TGarbageCollector.Instance.AddTempRoot(ResultsArray) and in the finally call
TGarbageCollector.Instance.RemoveTempRoot(ResultsArray) (guarded by
Assigned(ResultsArray) if needed) so RemoveTempRoot never receives an undefined
value; reference the ResultsArray variable, TGocciaArrayValue.Create,
TGarbageCollector.Instance.AddTempRoot and RemoveTempRoot when making the
change.
---
Outside diff comments:
In `@units/Goccia.Builtins.Benchmark.pas`:
- Around line 289-295: SetupResult returned from ABenchCase.SetupFunction.Call
is vulnerable to collection because TGocciaMicrotaskQueue.Instance.DrainQueue
can re-enter script code before the GC root is added; protect SetupResult
immediately after the Call by calling GC.AddTempRoot(SetupResult) before
invoking TGocciaMicrotaskQueue.Instance.DrainQueue and ensure you call
GC.RemoveTempRoot(SetupResult) after you no longer need it (e.g., after
computing Result.SetupMs and any subsequent use), so the root is held for the
full duration SetupResult is only referenced by Pascal code.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f095952d-165e-4949-a1bc-31d5efaee734
📒 Files selected for processing (6)
AGENTS.mddocs/architecture.mddocs/benchmarks.mddocs/design-decisions.mddocs/souffle-vm.mdunits/Goccia.Builtins.Benchmark.pas
- Fix confusing abbreviation example in AGENTS.md (TGenericGC → TGC) - Guard ResultsArray with nil init + Assigned check in Benchmark.pas - Show public GCMarked property instead of private field in value-system.md Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
units/Goccia.Builtins.Benchmark.pas (1)
286-295:⚠️ Potential issue | 🔴 CriticalRoot
SetupResultbefore draining microtasks.
SetupResultsits in Pascal-only storage after Line 289, butDrainQueuecan run before Line 295 roots it. If setup returns a GC-managed value and the drain triggers collection,RunArgscan end up holding a freed value.As per coding guidelines, "Values held only by Pascal code (not in any GocciaScript scope) must be protected with `AddTempRoot`/`RemoveTempRoot` for the duration they are needed."Suggested fix
StartNanoseconds := GetNanoseconds; SetupResult := ABenchCase.SetupFunction.Call(EmptyArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + if Assigned(SetupResult) and Assigned(GC) then + GC.AddTempRoot(SetupResult); if Assigned(TGocciaMicrotaskQueue.Instance) then TGocciaMicrotaskQueue.Instance.DrainQueue; Result.SetupMs := (GetNanoseconds - StartNanoseconds) / 1000000; - - if Assigned(SetupResult) and Assigned(GC) then - GC.AddTempRoot(SetupResult); end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@units/Goccia.Builtins.Benchmark.pas` around lines 286 - 295, SetupResult (returned by ABenchCase.SetupFunction.Call) can be collected during TGocciaMicrotaskQueue.Instance.DrainQueue because it lives only in Pascal stack; to fix, immediately guard SetupResult with GC.AddTempRoot after the Call and before calling TGocciaMicrotaskQueue.Instance.DrainQueue, and then call GC.RemoveTempRoot(SetupResult) once the value is no longer needed (e.g., after assigning into Result or at end of scope); update the block around ABenchCase.SetupFunction, SetupResult, TGocciaMicrotaskQueue.Instance.DrainQueue, GC.AddTempRoot and GC.RemoveTempRoot accordingly so the returned value is rooted during any microtask drain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/value-system.md`:
- Around line 118-125: The docs show TGocciaValue now inheriting from
TGCManagedObject but later examples (IsPrimitive and Virtual Property Access
snippets) still declare TGocciaValue = class(TInterfacedObject); update those
later code examples to use TGocciaValue = class(TGCManagedObject) and adjust any
constructor/cleanup semantics in the examples to match the unified GC model
(e.g., remove manual reference counting or Free calls and rely on
AfterConstruction/GC registration), ensuring all references to TGocciaValue,
IsPrimitive, and the Virtual Property Access snippets reflect the
TGCManagedObject inheritance.
---
Outside diff comments:
In `@units/Goccia.Builtins.Benchmark.pas`:
- Around line 286-295: SetupResult (returned by ABenchCase.SetupFunction.Call)
can be collected during TGocciaMicrotaskQueue.Instance.DrainQueue because it
lives only in Pascal stack; to fix, immediately guard SetupResult with
GC.AddTempRoot after the Call and before calling
TGocciaMicrotaskQueue.Instance.DrainQueue, and then call
GC.RemoveTempRoot(SetupResult) once the value is no longer needed (e.g., after
assigning into Result or at end of scope); update the block around
ABenchCase.SetupFunction, SetupResult,
TGocciaMicrotaskQueue.Instance.DrainQueue, GC.AddTempRoot and GC.RemoveTempRoot
accordingly so the returned value is rooted during any microtask drain.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 37024ea0-7e56-49c5-b838-575af6ce0423
📒 Files selected for processing (3)
AGENTS.mddocs/value-system.mdunits/Goccia.Builtins.Benchmark.pas
| TGocciaValue = class(TGCManagedObject) | ||
| procedure AfterConstruction; override; // Auto-registers with GC | ||
| procedure MarkReferences; virtual; // Override to mark referenced values | ||
| function RuntimeCopy: TGocciaValue; virtual; // Create a GC-managed copy | ||
| end; | ||
| ``` | ||
|
|
||
| - **`AfterConstruction`** — Every value auto-registers with `TGocciaGarbageCollector.Instance` upon creation. | ||
| - **`AfterConstruction`** — Every value auto-registers with `TGarbageCollector.Instance` upon creation. | ||
| - **`MarkReferences`** — Base implementation sets `GCMarked := True`. Subclasses override this to also mark values they reference (e.g., `TGocciaObjectValue` marks its prototype and property values, `TGocciaFunctionValue` marks its closure scope, `TGocciaArrayValue` marks its elements). |
There was a problem hiding this comment.
Update the later TGocciaValue snippets to match the unified GC model.
This section now documents TGocciaValue = class(TGCManagedObject), but the later examples under IsPrimitive and Virtual Property Access still show class(TInterfacedObject). That leaves the page self-contradictory for anyone using these snippets as reference.
📘 Suggested follow-up
-TGocciaValue = class(TInterfacedObject)
+TGocciaValue = class(TGCManagedObject)Apply that replacement in the later code examples as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/value-system.md` around lines 118 - 125, The docs show TGocciaValue now
inheriting from TGCManagedObject but later examples (IsPrimitive and Virtual
Property Access snippets) still declare TGocciaValue = class(TInterfacedObject);
update those later code examples to use TGocciaValue = class(TGCManagedObject)
and adjust any constructor/cleanup semantics in the examples to match the
unified GC model (e.g., remove manual reference counting or Free calls and rely
on AfterConstruction/GC registration), ensuring all references to TGocciaValue,
IsPrimitive, and the Virtual Property Access snippets reflect the
TGCManagedObject inheritance.
Unifies Goccia and Souffle garbage collectors into a single generic GC to fix Windows OOM and access violation errors in the benchmark runner.
The Windows 32-bit
BenchmarkRunnerwas hitting "Out of memory" because the Goccia GC never collected between benchmark files, leading to memory exhaustion. This PR unifies the Goccia and Souffle GCs, enabling explicit collection between files and resolving cross-GC dangling pointer issues, which also fixes pre-existingInvalid type casterrors in thetyped-arrays.jsbenchmark.Summary by CodeRabbit
New Features
Bug Fixes
Documentation