Skip to content

Create BindingMap for Scope and pre-typed array callbacks#29

Merged
frostney merged 3 commits into
mainfrom
feat-scope-binding-map
Feb 22, 2026
Merged

Create BindingMap for Scope and pre-typed array callbacks#29
frostney merged 3 commits into
mainfrom
feat-scope-binding-map

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 21, 2026

Summary by CodeRabbit

  • Performance Improvements
    • Faster array and collection operations via unified, optimized callback invocation and function fast-paths for common cases.
  • New Features
    • Typed callback argument support for array callbacks and reduce operations.
  • Infrastructure Improvements
    • New compact lexical binding registry for scopes, improving memory traversal and lookup.
    • Consistent typed-callback handling added to map/set/collection iteration for reliability and speed.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 21, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds callback-argument collection types and makes argument-collection methods overridable; introduces a new lexically-scoped binding map and migrates scope storage to it; routes array/map/set callbacks through a typed function fast-path with fallbacks; and adds function execution fast-paths for simple bodies.

Changes

Cohort / File(s) Summary
Callback argument types & collection base
units/Goccia.Arguments.Callbacks.pas, units/Goccia.Arguments.Collection.pas
New classes TGocciaArrayCallbackArgs and TGocciaReduceCallbackArgs providing fixed-length argument accessors; made TGocciaArgumentsCollection.GetElement and GetLength virtual to allow overrides.
Lexical binding map
units/Goccia.Scope.BindingMap.pas
New TLexicalBinding record and TGocciaBindingMap class implementing a compact resizable associative storage for lexical bindings, declaration types, and initialization flags.
Scope refactor to binding map
units/Goccia.Scope.pas
Replaced dictionary-based lexical storage with TGocciaBindingMap; updated constants, constructor/destructor, GC marking, name retrieval, and binding access/assignment to use the new map API and index-based iteration.
Array callback invocation & sorting
units/Goccia.Values.ArrayValue.pas
Refactored array methods (map/filter/reduce/forEach/some/every/flatMap/find*/sort) to prefer TGocciaFunctionBase typed calls using the new callback-arg containers and to pass comparator as TGocciaFunctionBase with fallback to generic invocation.
Map & Set callback invocation
units/Goccia.Values.MapValue.pas, units/Goccia.Values.SetValue.pas
Added typed callback fast-paths in forEach: cast to TGocciaFunctionBase and call directly when possible, otherwise fall back to InvokeCallable.
Function execution fast-paths
units/Goccia.Values.FunctionValue.pas
Introduced expression-body and single-return fast-paths and reorganized block execution and exception handling to optimize common function shapes while preserving semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through bindings, args in tow,

I stitch fast paths where returns flow,
I tuck each callback neat and small,
I guard each scope and name them all,
A little rabbit claps—code goes!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: introducing a BindingMap for Scope and implementing pre-typed array callbacks.
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-scope-binding-map

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
units/Goccia.Values.ArrayValue.pas (1)

1700-1711: ⚠️ Potential issue | 🔴 Critical

Add is TGocciaFunctionBase guard before casting in ArraySort and ArrayToSorted.

At lines 1708 and 1486, the callback is cast to TGocciaFunctionBase after only checking IsCallable. However, TGocciaClassValue is callable (overrides IsCallable) but inherits from TGocciaValue, not TGocciaFunctionBase. If a class value is passed as the comparator, the cast fails.

Other array methods (e.g., lines 465, 505, 559) properly guard with if Callback is TGocciaFunctionBase before accessing typed callback state. Apply the same guard here:

Suggested pattern
if CustomSortFunction.IsCallable then
begin
  if CustomSortFunction is TGocciaFunctionBase then
  begin
    // safe to cast
    QuickSortElements(Arr.Elements, TGocciaFunctionBase(CustomSortFunction), ...);
  end
  else
    ThrowError('Custom sort function must be a function');
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.ArrayValue.pas` around lines 1700 - 1711, The
ArraySort/ArrayToSorted code currently only checks CustomSortFunction.IsCallable
before casting to TGocciaFunctionBase and calling QuickSortElements; add a type
guard to verify CustomSortFunction is TGocciaFunctionBase (e.g., "if
CustomSortFunction is TGocciaFunctionBase") before performing the cast and
calling QuickSortElements, and otherwise call ThrowError('Custom sort function
must be a function'); apply the same pattern in both ArraySort and ArrayToSorted
so the cast to TGocciaFunctionBase is only done when the callback truly inherits
TGocciaFunctionBase.
🧹 Nitpick comments (2)
units/Goccia.Scope.BindingMap.pas (1)

170-173: Consider adding bounds validation or documenting the precondition.

GetValueAt lacks bounds checking, relying on callers to ensure 0 <= AIndex < FCount. While this is acceptable for the inline hot path in MarkReferences, the public Values[AIndex] property could be misused.

💡 Option: Add debug-only assertion
 function TGocciaBindingMap.GetValueAt(const AIndex: Integer): TGocciaValue;
 begin
+  Assert((AIndex >= 0) and (AIndex < FCount), 'BindingMap index out of bounds');
   Result := FValues[AIndex];
 end;

This provides safety during development without release build overhead (assertions are typically stripped in release mode).

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

In `@units/Goccia.Scope.BindingMap.pas` around lines 170 - 173,
TGocciaBindingMap.GetValueAt currently returns FValues[AIndex] with no bounds
validation; add a debug-only bounds assertion (e.g. Assert((AIndex >= 0) and
(AIndex < FCount))) at the start of TGocciaBindingMap.GetValueAt to catch misuse
during development without release overhead, or alternatively raise an
ERangeError for public usage; also update the Values property or method comment
to document the precondition (0 <= AIndex < FCount) and ensure callers like
MarkReferences remain inline/hot-path safe.
units/Goccia.Arguments.Callbacks.pas (1)

48-51: Constructors don't call inherited Create, leaving FArgs uninitialized.

Both constructors (TGocciaArrayCallbackArgs.Create and TGocciaReduceCallbackArgs.Create) skip calling inherited Create, so FArgs (from TGocciaArgumentsCollection) remains nil. This is intentional for the allocation-free design, but could cause issues if any inherited methods are called that access FArgs (e.g., Add, SetElement, Slice).

Since these classes only override GetElement and GetLength, and are used exclusively for callback invocation, this is acceptable—but consider adding a brief comment documenting this design choice.

📝 Add documentation comment
 constructor TGocciaArrayCallbackArgs.Create(const AThisArray: TGocciaValue);
 begin
+  // Intentionally skip inherited Create to avoid FArgs allocation.
+  // This class overrides GetElement/GetLength with fixed-structure access.
   FThisArray := AThisArray;
 end;

Also applies to: 71-74

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

In `@units/Goccia.Arguments.Callbacks.pas` around lines 48 - 51, The constructors
TGocciaArrayCallbackArgs.Create and TGocciaReduceCallbackArgs.Create
intentionally do not call inherited Create (so FArgs from
TGocciaArgumentsCollection stays nil); add a brief inline documentation comment
above each constructor explaining this deliberate allocation-free design and
that it's safe because these classes only override GetElement and GetLength and
are used solely for callback invocation, referencing FArgs and
TGocciaArgumentsCollection to make the rationale clear to future maintainers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@units/Goccia.Values.ArrayValue.pas`:
- Around line 1700-1711: The ArraySort/ArrayToSorted code currently only checks
CustomSortFunction.IsCallable before casting to TGocciaFunctionBase and calling
QuickSortElements; add a type guard to verify CustomSortFunction is
TGocciaFunctionBase (e.g., "if CustomSortFunction is TGocciaFunctionBase")
before performing the cast and calling QuickSortElements, and otherwise call
ThrowError('Custom sort function must be a function'); apply the same pattern in
both ArraySort and ArrayToSorted so the cast to TGocciaFunctionBase is only done
when the callback truly inherits TGocciaFunctionBase.

---

Nitpick comments:
In `@units/Goccia.Arguments.Callbacks.pas`:
- Around line 48-51: The constructors TGocciaArrayCallbackArgs.Create and
TGocciaReduceCallbackArgs.Create intentionally do not call inherited Create (so
FArgs from TGocciaArgumentsCollection stays nil); add a brief inline
documentation comment above each constructor explaining this deliberate
allocation-free design and that it's safe because these classes only override
GetElement and GetLength and are used solely for callback invocation,
referencing FArgs and TGocciaArgumentsCollection to make the rationale clear to
future maintainers.

In `@units/Goccia.Scope.BindingMap.pas`:
- Around line 170-173: TGocciaBindingMap.GetValueAt currently returns
FValues[AIndex] with no bounds validation; add a debug-only bounds assertion
(e.g. Assert((AIndex >= 0) and (AIndex < FCount))) at the start of
TGocciaBindingMap.GetValueAt to catch misuse during development without release
overhead, or alternatively raise an ERangeError for public usage; also update
the Values property or method comment to document the precondition (0 <= AIndex
< FCount) and ensure callers like MarkReferences remain inline/hot-path safe.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 21, 2026

Benchmark Results

179 benchmarks · 🟢 130 improved · 🔴 4 regressed · 45 unchanged · avg +21.7%

arrays.js — 🟢 15 improved, 🔴 3 regressed, 1 unchanged · avg +68.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 5,356 12,628 🟢 +135.8%
Array.from 10 elements 173,714 148,271 🔴 -14.6%
Array.of 10 elements 218,829 188,045 🔴 -14.1%
spread into new array 205,207 218,592 +6.5%
map over 50 elements 5,412 11,231 🟢 +107.5%
filter over 50 elements 5,187 9,648 🟢 +86.0%
reduce sum 50 elements 4,997 12,045 🟢 +141.0%
forEach over 50 elements 4,687 9,845 🟢 +110.1%
find in 50 elements 5,485 12,888 🟢 +135.0%
sort 20 elements 4,536 10,791 🟢 +137.9%
flat nested array 85,124 78,185 🔴 -8.2%
flatMap 43,165 52,976 🟢 +22.7%
map inside map (5x5) 11,332 15,419 🟢 +36.1%
filter inside map (5x10) 3,256 4,935 🟢 +51.6%
reduce inside map (5x10) 3,224 5,731 🟢 +77.8%
forEach inside forEach (5x10) 2,896 5,280 🟢 +82.3%
find inside some (10x10) 1,819 3,248 🟢 +78.6%
map+filter chain nested (5x20) 1,315 2,250 🟢 +71.2%
reduce flatten (10x5) 3,560 5,466 🟢 +53.5%
classes.js — 🟢 7 improved, 8 unchanged · avg +8.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 77,125 84,017 🟢 +8.9%
class with defaults 57,712 60,739 +5.2%
50 instances via Array.from 2,702 3,726 🟢 +37.9%
instance method call 36,382 35,570 -2.2%
static method call 61,479 70,773 🟢 +15.1%
single-level inheritance 29,606 28,931 -2.3%
two-level inheritance 26,275 27,005 +2.8%
private field access 37,752 40,397 🟢 +7.0%
private methods 43,164 44,177 +2.3%
getter/setter access 39,934 39,316 -1.5%
static getter read 71,490 75,919 +6.2%
static getter/setter pair 50,578 58,532 🟢 +15.7%
inherited static getter 42,814 44,647 +4.3%
inherited static setter 45,520 49,254 🟢 +8.2%
inherited static getter with this binding 34,691 38,738 🟢 +11.7%
closures.js — 🟢 11 improved · avg +21.7%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 63,467 70,810 🟢 +11.6%
closure over multiple variables 63,512 77,987 🟢 +22.8%
nested closures 68,261 83,832 🟢 +22.8%
function as argument 47,652 62,580 🟢 +31.3%
function returning function 62,526 78,656 🟢 +25.8%
compose two functions 38,266 45,917 🟢 +20.0%
fn.call 90,595 108,989 🟢 +20.3%
fn.apply 66,579 75,398 🟢 +13.2%
fn.bind 75,094 93,684 🟢 +24.8%
recursive sum to 50 5,661 6,984 🟢 +23.4%
recursive tree traversal 9,054 11,138 🟢 +23.0%
collections.js — 🟢 9 improved, 🔴 1 regressed, 2 unchanged · avg +21.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 3,394 4,932 🟢 +45.3%
has lookup (50 elements) 5,831 7,628 🟢 +30.8%
delete elements 15,259 19,534 🟢 +28.0%
forEach iteration 3,284 4,926 🟢 +50.0%
spread to array 6,621 7,935 🟢 +19.9%
deduplicate array 27,137 25,126 🔴 -7.4%
set 50 entries 2,725 3,496 🟢 +28.3%
get lookup (50 entries) 3,228 3,412 +5.7%
has check 3,120 3,609 🟢 +15.7%
delete entries 8,522 9,342 🟢 +9.6%
forEach iteration 2,145 2,812 🟢 +31.1%
keys/values/entries 2,602 2,567 -1.4%
destructuring.js — 🟢 11 improved, 3 unchanged · avg +14.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 219,460 244,638 🟢 +11.5%
with rest element 173,113 164,026 -5.2%
with defaults 221,165 240,239 🟢 +8.6%
skip elements 238,248 261,367 🟢 +9.7%
nested array destructuring 118,901 115,858 -2.6%
swap variables 223,847 299,526 🟢 +33.8%
simple object destructuring 150,874 169,753 🟢 +12.5%
with defaults 186,760 208,761 🟢 +11.8%
with renaming 165,235 200,878 🟢 +21.6%
nested object destructuring 82,688 92,541 🟢 +11.9%
rest properties 90,255 96,357 +6.8%
object parameter 50,930 63,010 🟢 +23.7%
array parameter 65,582 83,429 🟢 +27.2%
mixed destructuring in map 6,230 7,995 🟢 +28.3%
fibonacci.js — 🟢 5 improved, 1 unchanged · avg +18.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 156 195 🟢 +25.0%
recursive fib(20) 15 19 🟢 +29.4%
iterative fib(20) via reduce 7,818 7,825 +0.1%
iterator fib(20) 4,116 4,854 🟢 +17.9%
iterator fib(20) via Iterator.from + take 4,268 4,907 🟢 +15.0%
iterator fib(20) last value via reduce 3,432 4,256 🟢 +24.0%
iterators.js — 🟢 13 improved, 7 unchanged · avg +16.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
Iterator.from({next}).toArray() — 20 elements 5,770 5,825 +0.9%
Iterator.from({next}).toArray() — 50 elements 2,461 2,463 +0.1%
spread pre-wrapped iterator — 20 elements 5,677 5,852 +3.1%
Iterator.from({next}).forEach — 50 elements 1,792 2,052 🟢 +14.5%
Iterator.from({next}).reduce — 50 elements 1,777 2,020 🟢 +13.7%
wrap array iterator 32,586 32,895 +0.9%
wrap plain {next()} object 3,796 3,971 +4.6%
map + toArray (50 elements) 1,524 1,681 🟢 +10.3%
filter + toArray (50 elements) 1,599 1,732 🟢 +8.4%
take(10) + toArray (50 element source) 8,880 8,897 +0.2%
drop(40) + toArray (50 element source) 2,293 2,363 +3.1%
chained map + filter + take (100 element source) 2,671 3,018 🟢 +13.0%
some + every (50 elements) 979 1,127 🟢 +15.2%
find (50 elements) 2,129 2,492 🟢 +17.1%
array.values().map().filter().toArray() 1,500 1,991 🟢 +32.7%
array.values().take(5).toArray() 7,433 12,495 🟢 +68.1%
array.values().drop(45).toArray() 4,278 6,185 🟢 +44.6%
map.entries() chained helpers 1,855 2,401 🟢 +29.4%
set.values() chained helpers 3,213 3,919 🟢 +22.0%
string iterator map + toArray 4,033 4,980 🟢 +23.5%
json.js — 🟢 14 improved, 6 unchanged · avg +11.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 113,309 128,906 🟢 +13.8%
parse nested object 77,055 75,887 -1.5%
parse array of objects 41,747 42,166 +1.0%
parse large flat object 35,392 35,757 +1.0%
parse mixed types 53,693 56,160 +4.6%
stringify simple object 98,655 111,391 🟢 +12.9%
stringify nested object 58,469 61,715 +5.6%
stringify array of objects 11,011 12,621 🟢 +14.6%
stringify mixed types 46,861 50,865 🟢 +8.5%
reviver doubles numbers 23,180 26,646 🟢 +15.0%
reviver filters properties 22,087 25,803 🟢 +16.8%
reviver on nested object 26,819 33,381 🟢 +24.5%
reviver on array 16,852 20,401 🟢 +21.1%
replacer function doubles numbers 22,126 25,983 🟢 +17.4%
replacer function excludes properties 28,010 36,461 🟢 +30.2%
array replacer (allowlist) 59,105 66,191 🟢 +12.0%
stringify with 2-space indent 54,866 59,174 🟢 +7.9%
stringify with tab indent 54,489 59,011 🟢 +8.3%
parse then stringify 34,308 37,576 🟢 +9.5%
stringify then parse 12,961 13,271 +2.4%
jsx.jsx — 🟢 21 improved · avg +27.7%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple element 118,231 156,330 🟢 +32.2%
self-closing element 119,705 156,021 🟢 +30.3%
element with string attribute 101,956 124,165 🟢 +21.8%
element with multiple attributes 88,550 104,083 🟢 +17.5%
element with expression attribute 93,869 112,711 🟢 +20.1%
text child 115,250 152,429 🟢 +32.3%
expression child 107,245 141,569 🟢 +32.0%
mixed text and expression 103,358 139,940 🟢 +35.4%
nested elements (3 levels) 43,079 54,540 🟢 +26.6%
sibling children 32,366 40,511 🟢 +25.2%
component element 80,649 100,043 🟢 +24.0%
component with children 50,240 63,253 🟢 +25.9%
dotted component 68,149 82,202 🟢 +20.6%
empty fragment 105,923 148,934 🟢 +40.6%
fragment with children 30,942 38,944 🟢 +25.9%
spread attributes 59,436 74,504 🟢 +25.4%
spread with overrides 52,430 65,118 🟢 +24.2%
shorthand props 78,446 110,481 🟢 +40.8%
nav bar structure 15,583 19,494 🟢 +25.1%
card component tree 18,153 22,368 🟢 +23.2%
10 list items via Array.from 7,372 9,755 🟢 +32.3%
numbers.js — 🟢 8 improved, 3 unchanged · avg +21.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 206,064 306,215 🟢 +48.6%
floating point arithmetic 234,577 324,114 🟢 +38.2%
number coercion 106,779 129,826 🟢 +21.6%
toFixed 80,562 80,906 +0.4%
toString 105,909 109,387 +3.3%
valueOf 141,109 145,600 +3.2%
toPrecision 95,057 105,154 🟢 +10.6%
Number.isNaN 162,093 215,961 🟢 +33.2%
Number.isFinite 152,653 221,161 🟢 +44.9%
Number.isInteger 168,181 194,299 🟢 +15.5%
Number.parseInt and parseFloat 148,410 175,423 🟢 +18.2%
objects.js — 🟢 3 improved, 4 unchanged · avg +7.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 255,388 269,326 +5.5%
create nested object 126,586 128,622 +1.6%
create 50 objects via Array.from 4,903 5,882 🟢 +20.0%
property read 104,304 117,165 🟢 +12.3%
Object.keys 78,447 82,762 +5.5%
Object.entries 50,720 48,572 -4.2%
spread operator 95,183 103,205 🟢 +8.4%
promises.js — 🟢 3 improved, 9 unchanged · avg +3.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 335,612 348,868 +3.9%
new Promise(resolve => resolve(value)) 124,043 136,798 🟢 +10.3%
Promise.reject(reason) 319,796 356,517 🟢 +11.5%
resolve + then (1 handler) 105,912 112,396 +6.1%
resolve + then chain (3 deep) 45,779 47,451 +3.7%
resolve + then chain (10 deep) 14,624 15,596 +6.6%
reject + catch + then 60,829 66,362 🟢 +9.1%
resolve + finally + then 55,147 55,657 +0.9%
Promise.all (5 resolved) 22,725 21,693 -4.5%
Promise.race (5 resolved) 23,594 23,268 -1.4%
Promise.allSettled (5 mixed) 17,788 17,959 +1.0%
Promise.any (5 mixed) 22,756 21,723 -4.5%
strings.js — 🟢 10 improved, 1 unchanged · avg +18.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 273,256 342,550 🟢 +25.4%
template literal 250,816 369,600 🟢 +47.4%
string repeat 257,205 297,557 🟢 +15.7%
split and join 105,357 114,581 🟢 +8.8%
indexOf and includes 105,110 122,113 🟢 +16.2%
toUpperCase and toLowerCase 148,210 188,811 🟢 +27.4%
slice and substring 100,718 107,847 🟢 +7.1%
trim operations 115,151 132,689 🟢 +15.2%
replace and replaceAll 128,740 150,310 🟢 +16.8%
startsWith and endsWith 85,148 90,306 +6.1%
padStart and padEnd 124,044 144,560 🟢 +16.5%

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

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

🤖 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.Values.ArrayValue.pas`:
- Line 1706: The cast to TGocciaFunctionBase when calling QuickSortElements is
unsafe—before passing CustomSortFunction into QuickSortElements, check whether
CustomSortFunction actually supports TGocciaFunctionBase (e.g., using an "is" or
TryCast-style check) and only cast if valid; if the check fails, handle it
safely (either call a fallback comparison, raise a controlled error, or wrap the
callable in an adapter) so QuickSortElements receives a valid
TGocciaFunctionBase instance; update the call site where
QuickSortElements(Arr.Elements, TGocciaFunctionBase(CustomSortFunction),
CallArgs, AThisValue, 0, ...) is made to perform this type check and safe
handling (mirror the fix used in ArrayToSorted).
- Line 1484: The call to QuickSortElements casts CustomSortFunction to
TGocciaFunctionBase without checking its actual runtime type, which will crash
when CustomSortFunction is a TGocciaClassValue or another callable; before
casting, check the concrete type (e.g. use "is" to test for TGocciaFunctionBase)
and only pass a TGocciaFunctionBase instance to QuickSortElements, otherwise
handle the alternative callable (for example call a different comparator wrapper
or raise a clear error) so ResultArray.Elements sorting never performs an unsafe
cast.

CallArgs := TGocciaArgumentsCollection.Create([nil, nil]);
try
QuickSortElements(ResultArray.Elements, CustomSortFunction, CallArgs, AThisValue, 0, ResultArray.Elements.Count - 1);
QuickSortElements(ResultArray.Elements, TGocciaFunctionBase(CustomSortFunction), CallArgs, AThisValue, 0, ResultArray.Elements.Count - 1);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get context around line 1484 in the ArrayValue.pas file
echo "=== Context around line 1484 (the suspect code) ==="
sed -n '1475,1495p' units/Goccia.Values.ArrayValue.pas

echo ""
echo "=== Line 1479 to check the IsCallable check ==="
sed -n '1477,1486p' units/Goccia.Values.ArrayValue.pas

echo ""
echo "=== Check QuickSortElements signature ==="
sed -n '209,215p' units/Goccia.Values.ArrayValue.pas

Repository: frostney/GocciaScript

Length of output: 1570


🏁 Script executed:

#!/bin/bash
# Find all classes and check IsCallable
echo "=== Classes that define IsCallable property ==="
rg -n "procedure.*IsCallable|function.*IsCallable" -A 2 units/Goccia.Values*.pas 2>/dev/null | head -50

echo ""
echo "=== Check if TGocciaFunctionBase and related classes are defined ==="
rg -n "class.*TGoccia.*Function" units/Goccia.Values.FunctionBase.pas 2>/dev/null | head -20

echo ""
echo "=== Check VMT base class and its IsCallable ==="
fd -e pas . units/ | xargs grep -l "virtual.*IsCallable" 2>/dev/null | head -5

Repository: frostney/GocciaScript

Length of output: 1638


🏁 Script executed:

#!/bin/bash
# Search for where IsCallable is set to true to understand what types are callable
echo "=== Types that set IsCallable to True ==="
rg -n "IsCallable.*:=.*true|IsCallable.*:=.*True" -i units/Goccia.Values*.pas -B 5 | head -100

echo ""
echo "=== Check the value base class hierarchy ==="
rg -n "TGocciaValue\s*=\s*class" units/Goccia.Values.pas -A 20 | head -50

Repository: frostney/GocciaScript

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Verify exact hierarchy - search for class definitions related to functions
echo "=== Class definitions inheriting from TGocciaValue or TGocciaFunctionBase ==="
ast-grep --pattern 'class $NAME($BASE)' units/Goccia.Values.FunctionBase.pas | head -20

echo ""
echo "=== Check TGocciaValue.IsCallable implementation ==="
rg -n "IsCallable" units/Goccia.Values.pas -B 2 -A 5 | head -40

Repository: frostney/GocciaScript

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Check TGocciaClassValue class hierarchy
echo "=== TGocciaClassValue definition and inheritance ==="
rg -n "class TGocciaClassValue" units/ -A 5

echo ""
echo "=== TGocciaFunctionBase definition and inheritance ==="
rg -n "class TGocciaFunctionBase" units/ -A 5

echo ""
echo "=== All classes that override IsCallable ==="
rg -n "function IsCallable.*override" units/ -B 3

Repository: frostney/GocciaScript

Length of output: 906


🏁 Script executed:

#!/bin/bash
# Check if TGocciaClassValue is anywhere inherited from TGocciaFunctionBase
echo "=== Check full inheritance chain ==="
rg -n "TGocciaClassValue" units/Goccia.Values.ClassValue.pas | head -10

echo ""
echo "=== Search for all classes that may have IsCallable = True ==="
rg -n "procedure.*ObjectValue\|class.*Native.*Function" units/ | head -20

Repository: frostney/GocciaScript

Length of output: 734


Unsafe cast to TGocciaFunctionBase without type check.

At line 1484, the code assumes that IsCallable guarantees a TGocciaFunctionBase type, but this assumption is incorrect. Both TGocciaFunctionBase and TGocciaClassValue independently return true for IsCallable, yet TGocciaClassValue does not derive from TGocciaFunctionBase. The direct cast TGocciaFunctionBase(CustomSortFunction) will fail at runtime if the callable is a TGocciaClassValue or another callable type unrelated to TGocciaFunctionBase.

Proposed fix: Add type check before cast
     if not CustomSortFunction.IsCallable then
       ThrowError('Custom sort function must be a function');

+    if not (CustomSortFunction is TGocciaFunctionBase) then
+      ThrowError('Sort comparator must be a function');
+
     CallArgs := TGocciaArgumentsCollection.Create([nil, nil]);
     try
       QuickSortElements(ResultArray.Elements, TGocciaFunctionBase(CustomSortFunction), CallArgs, AThisValue, 0, ResultArray.Elements.Count - 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.ArrayValue.pas` at line 1484, The call to
QuickSortElements casts CustomSortFunction to TGocciaFunctionBase without
checking its actual runtime type, which will crash when CustomSortFunction is a
TGocciaClassValue or another callable; before casting, check the concrete type
(e.g. use "is" to test for TGocciaFunctionBase) and only pass a
TGocciaFunctionBase instance to QuickSortElements, otherwise handle the
alternative callable (for example call a different comparator wrapper or raise a
clear error) so ResultArray.Elements sorting never performs an unsafe cast.

CallArgs := TGocciaArgumentsCollection.Create([nil, nil]);
try
QuickSortElements(Arr.Elements, CustomSortFunction, CallArgs, AThisValue, 0, Arr.Elements.Count - 1);
QuickSortElements(Arr.Elements, TGocciaFunctionBase(CustomSortFunction), CallArgs, AThisValue, 0, Arr.Elements.Count - 1);
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

Same unsafe cast issue as in ArrayToSorted.

This line has the same problem as line 1484 - the cast to TGocciaFunctionBase may fail if CustomSortFunction is a callable that doesn't inherit from TGocciaFunctionBase.

Proposed fix: Add type check before cast
     if not CustomSortFunction.IsCallable then
       ThrowError('Custom sort function must be a function');

+    if not (CustomSortFunction is TGocciaFunctionBase) then
+      ThrowError('Sort comparator must be a function');
+
     CallArgs := TGocciaArgumentsCollection.Create([nil, nil]);
     try
       QuickSortElements(Arr.Elements, TGocciaFunctionBase(CustomSortFunction), CallArgs, AThisValue, 0, Arr.Elements.Count - 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.ArrayValue.pas` at line 1706, The cast to
TGocciaFunctionBase when calling QuickSortElements is unsafe—before passing
CustomSortFunction into QuickSortElements, check whether CustomSortFunction
actually supports TGocciaFunctionBase (e.g., using an "is" or TryCast-style
check) and only cast if valid; if the check fails, handle it safely (either call
a fallback comparison, raise a controlled error, or wrap the callable in an
adapter) so QuickSortElements receives a valid TGocciaFunctionBase instance;
update the call site where QuickSortElements(Arr.Elements,
TGocciaFunctionBase(CustomSortFunction), CallArgs, AThisValue, 0, ...) is made
to perform this type check and safe handling (mirror the fix used in
ArrayToSorted).

@frostney frostney merged commit f6a305d into main Feb 22, 2026
4 checks passed
@frostney frostney deleted the feat-scope-binding-map branch February 22, 2026 09:38
@frostney frostney added the performance Performance improvement label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant