Skip to content

Add parallel execution for multi-file runners#306

Merged
frostney merged 7 commits intomainfrom
t3code/multi-file-runner-concurrency
Apr 15, 2026
Merged

Add parallel execution for multi-file runners#306
frostney merged 7 commits intomainfrom
t3code/multi-file-runner-concurrency

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Apr 14, 2026

Summary

  • Adds parallel multi-file execution to ScriptLoader, TestRunner, and BenchmarkRunner with a new --jobs option and automatic worker-count selection.
  • Introduces thread pool/runtime support plus per-thread initialization to avoid shared mutable state races in builtins, call stack, GC, microtask queue, and related runtime objects.
  • Preserves ordered result aggregation on the main thread while collecting worker output, errors, and benchmark/test timing data safely across threads.
  • Updates benchmark reporting and several builtins to use thread-local static member storage instead of shared class state.

Testing

  • Suggested checks: ./build.pas testrunner && ./build/TestRunner tests
  • Suggested checks: ./build.pas loader && ./build/ScriptLoader path/to/multi-file/scripts --jobs=4
  • Suggested checks: ./build.pas benchmarkrunner && ./build/BenchmarkRunner benchmarks --jobs=4
  • Suggested checks: ./format.pas --check

- Run scripts, tests, and benchmarks across worker threads
- Move shared builtin state to thread-local storage
- Add `--jobs` selection and preserve ordered reporting
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae048f37-df7f-421c-8313-ada92b7dd147

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf539e and 436755f.

📒 Files selected for processing (4)
  • units/Goccia.Compiler.Scope.pas
  • units/Goccia.Compiler.Statements.pas
  • units/Goccia.Threading.Test.pas
  • units/Goccia.Threading.pas
👮 Files not reviewed due to content moderation or server errors (2)
  • units/Goccia.Threading.pas
  • units/Goccia.Threading.Test.pas

📝 Walkthrough

Walkthrough

Adds a thread-pool for parallel file execution, per-thread runtime initialization and prototype caching, a --jobs/-j CLI option, and converts many global/class-shared singletons/prototype caches to thread-local storage; runners dispatch work to worker threads then join results on the main thread preserving input order.

Changes

Cohort / File(s) Summary
Threading infra & init
units/Goccia.Threading.pas, units/Goccia.Threading.Init.pas
New TGocciaThreadPool, worker/result types, InitThreadRuntime/ShutdownThreadRuntime, RunAll, cancellation/coverage merge, and EnsureSharedPrototypesInitialized helper for per-thread builtin initialization.
Runners / Programs
BenchmarkRunner.dpr, ScriptLoader.dpr, TestRunner.dpr, REPL.dpr
Added conditional cthreads on UNIX, per-file worker procs, parallel run paths using TGocciaThreadPool, preallocated per-file result arrays, and main-thread join/ordering + adjusted progress output.
CLI option & job logic
units/Goccia.CLI.Application.pas
Added --jobs/-j option and GetJobCount logic with platform CPU detection and capping to file count.
Per-thread conversion — builtins & values
units/Goccia.Builtins.*, units/Goccia.Values.* (many files)
Replaced many class var/global prototype/member caches with threadvar equivalents; changed some open-array caches to TArray<TGocciaMemberDefinition>; kept lazy-init/registration but made them thread-local.
Core runtime singletons
units/Goccia.{GarbageCollector,CallStack,Profiler,Coverage,MicrotaskQueue,Timeout,RegExp.Runtime,ObjectModel}.pas
Moved singletons/internal runtime state to thread-local storage; Instance/Initialize/Shutdown operate per-thread; added TGocciaCoverageTracker.MergeFrom.
Worker result handling & reporting
units/Goccia.Benchmark.Reporter.pas, TestRunner.dpr
Added indexed file accessor and FileCount; workers populate plain thread-bound result records/arrays; main thread merges/aggregates results and optionally merges coverage.
Console & member caches
units/Goccia.Builtins.Console.pas, units/Goccia.Builtins.Global*.pas
Moved static member-definition caches to threadvar and register per-thread definitions during construction.
Timeouts / compiler state / object model
units/Goccia.Timeout.pas, units/Goccia.Compiler.*, units/Goccia.ObjectModel.pas
Converted several module-level counters and compiler stacks to threadvar, changed published-getter hosts to thread-local, removed unit finalization that freed shared globals.
Docs & examples
AGENTS.md, README.md, docs/{benchmarks,build-system,testing}.md
Documented --jobs usage and example commands for TestRunner, BenchmarkRunner, and ScriptLoader.
Tests
units/Goccia.Threading.Test.pas
New console test program exercising work-queue and thread-pool semantics, ordering, cancellation, and edge cases.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI (Main)
    participant Init as EnsureSharedPrototypesInitialized
    participant Pool as TGocciaThreadPool
    participant Worker as TGocciaFileWorker
    participant Main as Main Thread / Reporter

    CLI->>Init: EnsureSharedPrototypesInitialized(GlobalBuiltins)
    CLI->>Pool: Create(JobCount) / RunAll(Files, WorkerProc, WorkerData)
    Pool->>Worker: start (per-worker InitThreadRuntime)
    Worker->>Worker: for each dequeued file: call WorkerProc(file,index)
    Worker-->>Pool: append TGocciaWorkerResult(Index, Success, ErrorMessage, ConsoleOutput, Data)
    Worker->>Worker: ShutdownThreadRuntime (detach coverage)
    Pool-->>CLI: all workers complete, Results[] ordered by Index
    CLI->>Main: iterate Results[] in original order, Reporter.Add / Merge coverage
    Main-->>CLI: final summary/output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add parallel execution for multi-file runners' directly and accurately summarizes the main change across the entire changeset, which adds parallel execution support to multiple multi-file runner applications.
Description check ✅ Passed The PR description provides a clear summary of changes and includes a testing section with checkboxes and suggested commands, following the template structure with complete sections for both Summary and Testing.
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.


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

@coderabbitai coderabbitai Bot added performance Performance improvement internal Refactoring, CI, tooling, cleanup labels Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

Benchmark Results

364 benchmarks

Interpreted: 🟢 5 improved · 🔴 357 regressed · 2 unchanged · avg -43.2%
Bytecode: 🟢 2 improved · 🔴 360 regressed · 2 unchanged · avg -45.5%

arraybuffer.js — Interp: 🔴 14 · avg -51.2% · Bytecode: 🔴 14 · avg -51.5%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 482,006 ops/sec [479,612..483,460] → 220,975 ops/sec [200,531..228,666] 🔴 -54.2% 531,892 ops/sec [529,965..535,719] → 249,975 ops/sec [244,652..253,479] 🔴 -53.0%
create ArrayBuffer(64) 456,180 ops/sec [453,030..458,308] → 213,995 ops/sec [210,330..217,221] 🔴 -53.1% 504,324 ops/sec [501,608..508,598] → 237,289 ops/sec [233,902..241,009] 🔴 -52.9%
create ArrayBuffer(1024) 350,580 ops/sec [347,859..353,048] → 186,867 ops/sec [186,021..187,100] 🔴 -46.7% 378,407 ops/sec [376,205..381,990] → 195,770 ops/sec [193,172..196,035] 🔴 -48.3%
create ArrayBuffer(8192) 154,172 ops/sec [153,024..154,383] → 104,500 ops/sec [103,544..105,701] 🔴 -32.2% 161,597 ops/sec [160,216..162,776] → 96,842 ops/sec [94,523..97,863] 🔴 -40.1%
slice full buffer (64 bytes) 498,690 ops/sec [496,756..500,985] → 237,627 ops/sec [237,135..238,050] 🔴 -52.3% 614,291 ops/sec [611,415..619,337] → 282,872 ops/sec [279,493..284,135] 🔴 -54.0%
slice half buffer (512 of 1024 bytes) 428,118 ops/sec [427,549..429,116] → 208,942 ops/sec [204,069..213,606] 🔴 -51.2% 529,743 ops/sec [528,827..530,742] → 247,621 ops/sec [244,975..249,266] 🔴 -53.3%
slice with negative indices 421,736 ops/sec [420,278..423,945] → 196,017 ops/sec [193,768..197,513] 🔴 -53.5% 573,415 ops/sec [570,402..578,265] → 266,368 ops/sec [256,618..268,515] 🔴 -53.5%
slice empty range 477,987 ops/sec [475,455..478,747] → 221,073 ops/sec [219,116..225,007] 🔴 -53.7% 607,913 ops/sec [602,327..609,396] → 272,694 ops/sec [270,477..280,731] 🔴 -55.1%
byteLength access 1,318,110 ops/sec [1,314,097..1,319,762] → 591,785 ops/sec [590,387..592,342] 🔴 -55.1% 1,794,715 ops/sec [1,789,742..1,799,117] → 772,451 ops/sec [767,422..774,613] 🔴 -57.0%
Symbol.toStringTag access 1,009,804 ops/sec [1,007,913..1,012,675] → 431,928 ops/sec [431,551..432,459] 🔴 -57.2% 1,069,167 ops/sec [1,065,096..1,072,028] → 502,093 ops/sec [500,480..503,522] 🔴 -53.0%
ArrayBuffer.isView 739,397 ops/sec [725,598..745,953] → 310,079 ops/sec [304,576..314,354] 🔴 -58.1% 856,811 ops/sec [855,257..858,690] → 393,061 ops/sec [391,469..397,663] 🔴 -54.1%
clone ArrayBuffer(64) 426,626 ops/sec [425,986..428,138] → 202,298 ops/sec [201,712..202,581] 🔴 -52.6% 498,608 ops/sec [497,096..501,131] → 245,008 ops/sec [240,891..246,489] 🔴 -50.9%
clone ArrayBuffer(1024) 331,170 ops/sec [330,281..332,127] → 175,075 ops/sec [173,778..175,558] 🔴 -47.1% 373,754 ops/sec [371,455..375,406] → 198,219 ops/sec [197,079..200,448] 🔴 -47.0%
clone ArrayBuffer inside object 269,644 ops/sec [269,325..270,239] → 137,343 ops/sec [136,610..137,728] 🔴 -49.1% 299,915 ops/sec [267,201..302,815] → 153,963 ops/sec [153,416..154,465] 🔴 -48.7%
arrays.js — Interp: 🔴 19 · avg -53.5% · Bytecode: 🔴 19 · avg -54.6%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 13,280 ops/sec [13,212..13,290] → 5,889 ops/sec [5,664..5,897] 🔴 -55.7% 17,424 ops/sec [17,103..17,574] → 7,242 ops/sec [7,142..7,288] 🔴 -58.4%
Array.from 10 elements 225,323 ops/sec [224,689..225,783] → 107,106 ops/sec [106,868..107,544] 🔴 -52.5% 242,926 ops/sec [240,584..243,807] → 108,195 ops/sec [107,759..108,366] 🔴 -55.5%
Array.of 10 elements 276,806 ops/sec [276,405..277,135] → 131,871 ops/sec [130,656..132,469] 🔴 -52.4% 307,558 ops/sec [306,902..308,271] → 138,638 ops/sec [138,128..140,056] 🔴 -54.9%
spread into new array 339,212 ops/sec [338,672..339,832] → 162,736 ops/sec [159,823..169,275] 🔴 -52.0% 200,006 ops/sec [199,470..201,097] → 87,692 ops/sec [84,859..90,537] 🔴 -56.2%
map over 50 elements 24,817 ops/sec [24,765..24,897] → 11,430 ops/sec [11,243..11,524] 🔴 -53.9% 33,500 ops/sec [33,331..33,725] → 14,607 ops/sec [14,419..14,816] 🔴 -56.4%
filter over 50 elements 22,778 ops/sec [22,738..22,830] → 10,321 ops/sec [10,265..10,380] 🔴 -54.7% 29,843 ops/sec [29,574..30,401] → 14,566 ops/sec [14,378..14,687] 🔴 -51.2%
reduce sum 50 elements 25,824 ops/sec [25,748..25,977] → 11,382 ops/sec [11,335..11,444] 🔴 -55.9% 32,188 ops/sec [32,121..32,358] → 14,059 ops/sec [14,035..14,159] 🔴 -56.3%
forEach over 50 elements 23,031 ops/sec [22,974..23,074] → 9,951 ops/sec [9,939..9,978] 🔴 -56.8% 31,911 ops/sec [31,232..31,975] → 13,784 ops/sec [13,603..13,822] 🔴 -56.8%
find in 50 elements 32,552 ops/sec [32,523..32,601] → 14,786 ops/sec [14,724..14,846] 🔴 -54.6% 47,423 ops/sec [46,877..47,653] → 20,965 ops/sec [20,886..21,108] 🔴 -55.8%
sort 20 elements 12,309 ops/sec [12,239..12,358] → 5,587 ops/sec [5,555..5,646] 🔴 -54.6% 17,132 ops/sec [17,119..17,200] → 7,932 ops/sec [7,833..7,979] 🔴 -53.7%
flat nested array 117,280 ops/sec [116,606..117,761] → 56,512 ops/sec [56,341..56,605] 🔴 -51.8% 129,023 ops/sec [128,204..129,519] → 58,207 ops/sec [57,435..58,834] 🔴 -54.9%
flatMap 74,679 ops/sec [74,351..74,765] → 36,663 ops/sec [36,392..36,753] 🔴 -50.9% 86,526 ops/sec [86,444..87,535] → 39,913 ops/sec [39,699..40,062] 🔴 -53.9%
map inside map (5x5) 20,327 ops/sec [20,165..20,426] → 9,756 ops/sec [9,676..9,832] 🔴 -52.0% 24,879 ops/sec [24,819..24,934] → 11,273 ops/sec [10,928..11,463] 🔴 -54.7%
filter inside map (5x10) 15,479 ops/sec [15,399..15,507] → 7,285 ops/sec [7,242..7,346] 🔴 -52.9% 19,258 ops/sec [19,005..19,593] → 9,481 ops/sec [9,365..9,554] 🔴 -50.8%
reduce inside map (5x10) 18,971 ops/sec [18,905..19,024] → 8,673 ops/sec [8,612..8,766] 🔴 -54.3% 23,342 ops/sec [23,188..23,359] → 10,726 ops/sec [10,608..10,850] 🔴 -54.0%
forEach inside forEach (5x10) 16,895 ops/sec [16,799..17,017] → 7,405 ops/sec [7,310..7,444] 🔴 -56.2% 24,519 ops/sec [24,442..24,556] → 10,736 ops/sec [10,582..10,924] 🔴 -56.2%
find inside some (10x10) 13,345 ops/sec [13,299..13,383] → 6,154 ops/sec [6,127..6,179] 🔴 -53.9% 17,817 ops/sec [17,765..17,941] → 8,340 ops/sec [8,048..8,425] 🔴 -53.2%
map+filter chain nested (5x20) 5,147 ops/sec [5,145..5,154] → 2,371 ops/sec [2,363..2,374] 🔴 -53.9% 6,696 ops/sec [6,557..6,744] → 3,145 ops/sec [3,131..3,163] 🔴 -53.0%
reduce flatten (10x5) 38,716 ops/sec [38,577..38,921] → 20,325 ops/sec [20,277..20,383] 🔴 -47.5% 17,914 ops/sec [17,838..18,001] → 8,554 ops/sec [8,469..8,786] 🔴 -52.2%
async-await.js — Interp: 🔴 6 · avg -52.5% · Bytecode: 🔴 6 · avg -53.9%
Benchmark Interpreted Δ Bytecode Δ
single await 385,452 ops/sec [384,629..390,211] → 183,080 ops/sec [167,220..187,542] 🔴 -52.5% 404,618 ops/sec [403,897..406,789] → 189,467 ops/sec [187,910..191,196] 🔴 -53.2%
multiple awaits 174,548 ops/sec [173,679..174,713] → 85,090 ops/sec [82,020..85,483] 🔴 -51.3% 173,462 ops/sec [172,402..174,002] → 81,955 ops/sec [81,367..82,458] 🔴 -52.8%
await non-Promise value 859,478 ops/sec [858,021..860,429] → 389,761 ops/sec [382,390..391,665] 🔴 -54.7% 1,162,965 ops/sec [1,156,912..1,174,246] → 499,884 ops/sec [491,317..504,477] 🔴 -57.0%
await with try/catch 373,770 ops/sec [372,943..375,826] → 175,766 ops/sec [173,495..176,527] 🔴 -53.0% 399,269 ops/sec [396,673..400,360] → 175,585 ops/sec [173,799..176,231] 🔴 -56.0%
await Promise.all 51,535 ops/sec [51,411..51,544] → 25,148 ops/sec [25,048..25,409] 🔴 -51.2% 50,790 ops/sec [50,561..50,922] → 24,697 ops/sec [24,492..24,835] 🔴 -51.4%
nested async function call 197,268 ops/sec [195,791..197,678] → 93,681 ops/sec [92,077..95,061] 🔴 -52.5% 238,666 ops/sec [236,618..239,815] → 112,283 ops/sec [112,034..112,953] 🔴 -53.0%
base64.js — Interp: 🔴 10 · avg -51.5% · Bytecode: 🔴 10 · avg -51.0%
Benchmark Interpreted Δ Bytecode Δ
short ASCII (13 chars) 530,105 ops/sec [520,140..535,425] → 227,710 ops/sec [175,020..252,851] 🔴 -57.0% 633,209 ops/sec [630,427..635,897] → 271,466 ops/sec [201,436..289,536] 🔴 -57.1%
medium ASCII (450 chars) 152,195 ops/sec [151,892..152,683] → 85,277 ops/sec [84,842..85,534] 🔴 -44.0% 159,372 ops/sec [159,319..159,529] → 96,257 ops/sec [91,948..98,081] 🔴 -39.6%
Latin-1 characters 549,640 ops/sec [542,062..561,905] → 242,708 ops/sec [242,232..243,587] 🔴 -55.8% 651,307 ops/sec [638,834..653,904] → 292,893 ops/sec [287,490..295,381] 🔴 -55.0%
short base64 (20 chars) 384,824 ops/sec [377,913..388,327] → 180,098 ops/sec [179,274..182,264] 🔴 -53.2% 439,795 ops/sec [438,976..440,251] → 202,546 ops/sec [201,142..203,230] 🔴 -53.9%
medium base64 (600 chars) 83,459 ops/sec [83,372..84,019] → 46,486 ops/sec [46,233..46,704] 🔴 -44.3% 85,255 ops/sec [85,090..85,808] → 46,378 ops/sec [45,059..46,698] 🔴 -45.6%
Latin-1 output 404,774 ops/sec [400,497..409,838] → 186,299 ops/sec [184,376..187,421] 🔴 -54.0% 465,481 ops/sec [464,706..466,627] → 213,057 ops/sec [207,359..219,180] 🔴 -54.2%
forgiving (no padding) 435,731 ops/sec [427,982..439,455] → 200,211 ops/sec [195,676..203,668] 🔴 -54.1% 446,280 ops/sec [445,822..448,287] → 204,279 ops/sec [198,767..206,335] 🔴 -54.2%
with whitespace 401,438 ops/sec [397,371..405,205] → 182,770 ops/sec [181,705..185,287] 🔴 -54.5% 406,287 ops/sec [404,152..406,980] → 189,585 ops/sec [187,119..190,883] 🔴 -53.3%
atob(btoa(short)) 254,824 ops/sec [251,673..255,848] → 116,052 ops/sec [115,646..116,984] 🔴 -54.5% 276,260 ops/sec [275,789..277,355] → 127,877 ops/sec [127,158..128,523] 🔴 -53.7%
atob(btoa(medium)) 55,237 ops/sec [54,967..55,414] → 31,331 ops/sec [31,194..31,386] 🔴 -43.3% 56,165 ops/sec [56,074..56,314] → 31,921 ops/sec [31,666..32,362] 🔴 -43.2%
classes.js — Interp: 🔴 31 · avg -47.0% · Bytecode: 🔴 31 · avg -48.2%
Benchmark Interpreted Δ Bytecode Δ
simple class new 140,915 ops/sec [140,245..141,519] → 71,392 ops/sec [70,833..71,685] 🔴 -49.3% 210,738 ops/sec [209,816..212,139] → 98,062 ops/sec [97,834..99,339] 🔴 -53.5%
class with defaults 112,174 ops/sec [111,598..112,560] → 56,632 ops/sec [56,083..57,285] 🔴 -49.5% 140,980 ops/sec [139,810..142,558] → 65,530 ops/sec [65,010..66,007] 🔴 -53.5%
50 instances via Array.from 5,739 ops/sec [5,683..5,774] → 2,766 ops/sec [2,697..2,794] 🔴 -51.8% 8,411 ops/sec [8,379..8,489] → 4,116 ops/sec [4,063..4,131] 🔴 -51.1%
instance method call 71,842 ops/sec [71,629..72,169] → 35,810 ops/sec [35,251..35,940] 🔴 -50.2% 98,841 ops/sec [98,125..99,391] → 47,647 ops/sec [44,121..47,926] 🔴 -51.8%
static method call 113,741 ops/sec [113,611..114,103] → 57,776 ops/sec [56,399..57,929] 🔴 -49.2% 188,548 ops/sec [187,802..190,772] → 90,332 ops/sec [88,203..91,891] 🔴 -52.1%
single-level inheritance 56,694 ops/sec [56,371..57,243] → 28,703 ops/sec [28,462..28,877] 🔴 -49.4% 72,986 ops/sec [72,703..74,213] → 35,249 ops/sec [35,021..35,328] 🔴 -51.7%
two-level inheritance 49,135 ops/sec [48,886..49,282] → 24,982 ops/sec [24,824..25,068] 🔴 -49.2% 59,957 ops/sec [58,347..61,077] → 28,886 ops/sec [28,626..29,370] 🔴 -51.8%
private field access 72,639 ops/sec [71,909..72,816] → 37,922 ops/sec [37,308..38,802] 🔴 -47.8% 94,924 ops/sec [94,061..95,547] → 47,347 ops/sec [47,036..47,530] 🔴 -50.1%
private methods 79,836 ops/sec [79,596..80,109] → 41,168 ops/sec [40,978..41,792] 🔴 -48.4% 99,661 ops/sec [98,613..100,232] → 50,584 ops/sec [49,695..51,057] 🔴 -49.2%
getter/setter access 76,811 ops/sec [76,468..77,075] → 38,769 ops/sec [38,517..39,054] 🔴 -49.5% 107,074 ops/sec [106,183..107,392] → 52,682 ops/sec [51,998..54,003] 🔴 -50.8%
class decorator (identity) 95,729 ops/sec [94,399..96,322] → 49,032 ops/sec [48,718..49,363] 🔴 -48.8% 113,754 ops/sec [112,755..115,877] → 53,393 ops/sec [52,549..54,117] 🔴 -53.1%
class decorator (wrapping) 55,942 ops/sec [55,570..56,364] → 29,018 ops/sec [28,659..29,548] 🔴 -48.1% 58,715 ops/sec [58,028..59,778] → 30,039 ops/sec [29,805..30,097] 🔴 -48.8%
identity method decorator 66,736 ops/sec [66,515..67,242] → 35,903 ops/sec [35,326..36,385] 🔴 -46.2% 90,723 ops/sec [83,513..97,171] → 45,284 ops/sec [42,874..46,760] 🔴 -50.1%
wrapping method decorator 52,854 ops/sec [52,729..53,102] → 28,494 ops/sec [28,119..29,071] 🔴 -46.1% 64,089 ops/sec [60,718..69,100] → 32,971 ops/sec [31,883..34,262] 🔴 -48.6%
stacked method decorators (x3) 35,487 ops/sec [35,033..35,919] → 19,319 ops/sec [19,202..19,568] 🔴 -45.6% 47,366 ops/sec [44,794..50,729] → 22,281 ops/sec [21,354..22,628] 🔴 -53.0%
identity field decorator 73,379 ops/sec [72,526..74,025] → 38,686 ops/sec [38,052..39,666] 🔴 -47.3% 74,626 ops/sec [73,926..76,966] → 40,468 ops/sec [39,609..41,800] 🔴 -45.8%
field initializer decorator 60,497 ops/sec [60,195..60,956] → 32,808 ops/sec [32,383..33,412] 🔴 -45.8% 65,556 ops/sec [63,622..69,941] → 34,432 ops/sec [33,888..35,043] 🔴 -47.5%
getter decorator (identity) 65,066 ops/sec [63,614..66,411] → 37,324 ops/sec [37,031..37,473] 🔴 -42.6% 73,329 ops/sec [71,336..74,043] → 39,349 ops/sec [38,754..39,369] 🔴 -46.3%
setter decorator (identity) 55,137 ops/sec [54,739..55,397] → 29,692 ops/sec [29,555..29,899] 🔴 -46.1% 61,539 ops/sec [60,996..62,161] → 32,712 ops/sec [32,481..32,876] 🔴 -46.8%
static method decorator 72,116 ops/sec [71,962..72,462] → 37,691 ops/sec [37,113..38,361] 🔴 -47.7% 81,837 ops/sec [80,494..84,458] → 48,031 ops/sec [44,988..49,533] 🔴 -41.3%
static field decorator 83,677 ops/sec [83,005..84,828] → 44,684 ops/sec [43,935..46,077] 🔴 -46.6% 83,553 ops/sec [80,331..89,460] → 46,096 ops/sec [45,881..46,161] 🔴 -44.8%
private method decorator 53,762 ops/sec [53,561..53,858] → 29,775 ops/sec [29,371..30,388] 🔴 -44.6% 72,248 ops/sec [68,790..75,578] → 38,743 ops/sec [37,910..40,359] 🔴 -46.4%
private field decorator 59,230 ops/sec [58,810..59,388] → 32,207 ops/sec [31,734..32,459] 🔴 -45.6% 61,506 ops/sec [60,817..63,258] → 33,314 ops/sec [32,736..33,528] 🔴 -45.8%
plain auto-accessor (no decorator) 105,297 ops/sec [103,566..106,626] → 54,884 ops/sec [53,949..58,132] 🔴 -47.9% 96,326 ops/sec [94,692..101,063] → 52,450 ops/sec [50,376..53,266] 🔴 -45.6%
auto-accessor with decorator 55,390 ops/sec [54,998..56,538] → 31,132 ops/sec [30,410..32,383] 🔴 -43.8% 55,775 ops/sec [54,043..60,770] → 31,159 ops/sec [29,781..32,449] 🔴 -44.1%
decorator writing metadata 45,235 ops/sec [44,840..45,568] → 25,399 ops/sec [24,605..25,774] 🔴 -43.9% 49,719 ops/sec [47,910..54,252] → 26,213 ops/sec [26,060..28,001] 🔴 -47.3%
static getter read 128,752 ops/sec [126,129..129,179] → 70,998 ops/sec [70,747..71,288] 🔴 -44.9% 167,243 ops/sec [161,606..169,252] → 93,941 ops/sec [93,563..97,628] 🔴 -43.8%
static getter/setter pair 99,122 ops/sec [98,430..99,621] → 53,361 ops/sec [53,167..53,662] 🔴 -46.2% 128,639 ops/sec [126,971..134,370] → 67,118 ops/sec [66,373..70,682] 🔴 -47.8%
inherited static getter 77,764 ops/sec [77,480..77,942] → 42,839 ops/sec [42,547..43,201] 🔴 -44.9% 96,538 ops/sec [93,952..97,062] → 53,100 ops/sec [52,715..53,438] 🔴 -45.0%
inherited static setter 84,300 ops/sec [83,863..85,062] → 46,928 ops/sec [46,604..47,050] 🔴 -44.3% 99,837 ops/sec [98,271..101,211] → 56,712 ops/sec [55,866..57,272] 🔴 -43.2%
inherited static getter with this binding 72,774 ops/sec [72,552..72,907] → 39,483 ops/sec [39,122..39,879] 🔴 -45.7% 87,195 ops/sec [86,508..87,976] → 49,716 ops/sec [48,733..50,211] 🔴 -43.0%
closures.js — Interp: 🔴 11 · avg -50.1% · Bytecode: 🔴 11 · avg -52.0%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 136,333 ops/sec [135,788..136,717] → 65,731 ops/sec [65,456..65,906] 🔴 -51.8% 321,185 ops/sec [318,616..322,691] → 152,304 ops/sec [150,362..154,212] 🔴 -52.6%
closure over multiple variables 123,380 ops/sec [122,829..123,630] → 61,088 ops/sec [60,450..61,326] 🔴 -50.5% 320,059 ops/sec [319,105..321,300] → 150,190 ops/sec [149,220..150,670] 🔴 -53.1%
nested closures 129,423 ops/sec [128,904..129,992] → 66,404 ops/sec [65,637..66,597] 🔴 -48.7% 290,883 ops/sec [286,063..300,046] → 140,079 ops/sec [137,787..141,566] 🔴 -51.8%
function as argument 94,982 ops/sec [93,138..95,100] → 45,750 ops/sec [45,566..46,359] 🔴 -51.8% 318,594 ops/sec [314,803..324,341] → 146,129 ops/sec [144,798..147,822] 🔴 -54.1%
function returning function 118,461 ops/sec [118,207..118,936] → 60,165 ops/sec [60,078..60,504] 🔴 -49.2% 354,781 ops/sec [339,053..364,330] → 164,419 ops/sec [161,544..165,385] 🔴 -53.7%
compose two functions 71,936 ops/sec [71,745..72,050] → 36,013 ops/sec [35,855..36,045] 🔴 -49.9% 187,205 ops/sec [185,779..195,851] → 94,829 ops/sec [93,804..95,080] 🔴 -49.3%
fn.call 153,867 ops/sec [151,284..154,397] → 77,592 ops/sec [74,809..78,173] 🔴 -49.6% 216,261 ops/sec [213,285..223,309] → 105,888 ops/sec [105,005..106,948] 🔴 -51.0%
fn.apply 113,248 ops/sec [111,912..114,869] → 59,189 ops/sec [58,461..59,864] 🔴 -47.7% 198,128 ops/sec [195,906..198,532] → 106,209 ops/sec [105,285..107,178] 🔴 -46.4%
fn.bind 144,012 ops/sec [143,837..144,731] → 74,173 ops/sec [73,765..74,658] 🔴 -48.5% 368,167 ops/sec [360,532..373,170] → 186,185 ops/sec [183,600..188,199] 🔴 -49.4%
recursive sum to 50 12,209 ops/sec [11,976..12,365] → 5,620 ops/sec [5,469..5,648] 🔴 -54.0% 48,840 ops/sec [48,027..48,982] → 20,632 ops/sec [20,539..20,667] 🔴 -57.8%
recursive tree traversal 20,642 ops/sec [20,083..20,842] → 10,474 ops/sec [10,417..10,533] 🔴 -49.3% 43,101 ops/sec [42,961..43,476] → 20,242 ops/sec [19,783..20,605] 🔴 -53.0%
collections.js — Interp: 🔴 12 · avg -44.5% · Bytecode: 🔴 12 · avg -49.6%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 6,945 ops/sec [6,744..7,016] → 3,883 ops/sec [3,864..3,892] 🔴 -44.1% 7,352 ops/sec [7,321..7,356] → 3,815 ops/sec [3,784..3,831] 🔴 -48.1%
has lookup (50 elements) 88,018 ops/sec [86,977..88,325] → 52,040 ops/sec [51,312..52,533] 🔴 -40.9% 96,438 ops/sec [96,371..96,552] → 50,628 ops/sec [50,461..51,047] 🔴 -47.5%
delete elements 47,053 ops/sec [46,567..47,723] → 28,713 ops/sec [28,446..28,865] 🔴 -39.0% 48,724 ops/sec [48,473..48,829] → 26,346 ops/sec [26,179..26,356] 🔴 -45.9%
forEach iteration 15,428 ops/sec [15,399..15,638] → 7,426 ops/sec [7,306..7,516] 🔴 -51.9% 21,631 ops/sec [21,615..21,661] → 9,487 ops/sec [9,385..9,808] 🔴 -56.1%
spread to array 29,303 ops/sec [29,038..29,789] → 17,014 ops/sec [16,943..17,077] 🔴 -41.9% 230,303 ops/sec [226,003..234,163] → 116,615 ops/sec [115,871..117,125] 🔴 -49.4%
deduplicate array 39,052 ops/sec [38,723..39,379] → 23,528 ops/sec [23,490..23,605] 🔴 -39.8% 70,716 ops/sec [69,526..72,810] → 37,169 ops/sec [36,937..37,509] 🔴 -47.4%
set 50 entries 5,281 ops/sec [5,236..5,539] → 2,861 ops/sec [2,807..2,915] 🔴 -45.8% 5,691 ops/sec [5,612..5,761] → 2,810 ops/sec [2,765..2,837] 🔴 -50.6%
get lookup (50 entries) 88,370 ops/sec [85,046..88,683] → 48,187 ops/sec [47,835..48,993] 🔴 -45.5% 89,766 ops/sec [89,139..89,990] → 44,495 ops/sec [44,276..44,780] 🔴 -50.4%
has check 128,480 ops/sec [126,011..130,036] → 70,777 ops/sec [70,136..71,438] 🔴 -44.9% 138,283 ops/sec [137,524..138,484] → 67,835 ops/sec [66,716..68,284] 🔴 -50.9%
delete entries 46,830 ops/sec [46,519..47,577] → 26,939 ops/sec [26,784..27,138] 🔴 -42.5% 46,727 ops/sec [46,404..46,937] → 23,565 ops/sec [23,471..23,706] 🔴 -49.6%
forEach iteration 15,951 ops/sec [15,908..16,023] → 7,278 ops/sec [7,084..7,469] 🔴 -54.4% 22,148 ops/sec [22,080..22,273] → 9,520 ops/sec [9,358..9,532] 🔴 -57.0%
keys/values/entries 7,877 ops/sec [7,859..7,904] → 4,475 ops/sec [4,428..4,512] 🔴 -43.2% 25,866 ops/sec [24,796..26,154] → 14,970 ops/sec [14,857..15,102] 🔴 -42.1%
destructuring.js — Interp: 🔴 22 · avg -45.8% · Bytecode: 🔴 22 · avg -48.0%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 402,885 ops/sec [400,485..404,081] → 204,195 ops/sec [200,335..206,844] 🔴 -49.3% 261,376 ops/sec [258,914..266,948] → 139,378 ops/sec [135,027..140,522] 🔴 -46.7%
with rest element 250,277 ops/sec [247,976..250,443] → 134,994 ops/sec [131,741..135,966] 🔴 -46.1% 202,546 ops/sec [193,737..205,532] → 102,982 ops/sec [101,234..105,303] 🔴 -49.2%
with defaults 394,776 ops/sec [393,224..395,265] → 212,096 ops/sec [210,515..215,236] 🔴 -46.3% 303,170 ops/sec [299,665..308,143] → 166,167 ops/sec [164,044..167,912] 🔴 -45.2%
skip elements 420,634 ops/sec [417,129..421,844] → 221,357 ops/sec [214,948..222,839] 🔴 -47.4% 283,466 ops/sec [275,622..291,583] → 150,030 ops/sec [148,649..150,943] 🔴 -47.1%
nested array destructuring 175,106 ops/sec [173,484..175,607] → 97,021 ops/sec [96,037..97,247] 🔴 -44.6% 88,086 ops/sec [84,814..89,154] → 48,166 ops/sec [47,361..48,251] 🔴 -45.3%
swap variables 522,090 ops/sec [517,807..527,409] → 276,810 ops/sec [274,519..278,573] 🔴 -47.0% 332,757 ops/sec [325,115..335,726] → 175,751 ops/sec [174,784..176,394] 🔴 -47.2%
simple object destructuring 311,422 ops/sec [309,832..312,595] → 167,106 ops/sec [162,258..172,008] 🔴 -46.3% 380,978 ops/sec [370,821..382,280] → 184,055 ops/sec [183,529..184,218] 🔴 -51.7%
with defaults 346,779 ops/sec [345,056..348,264] → 190,879 ops/sec [188,998..192,302] 🔴 -45.0% 519,043 ops/sec [511,365..523,126] → 250,625 ops/sec [247,822..252,780] 🔴 -51.7%
with renaming 329,757 ops/sec [328,322..331,950] → 184,198 ops/sec [182,463..185,114] 🔴 -44.1% 380,747 ops/sec [377,384..384,312] → 189,211 ops/sec [186,630..190,228] 🔴 -50.3%
nested object destructuring 153,411 ops/sec [153,187..154,130] → 84,588 ops/sec [83,548..87,891] 🔴 -44.9% 172,914 ops/sec [166,521..174,103] → 93,014 ops/sec [91,306..93,159] 🔴 -46.2%
rest properties 185,804 ops/sec [185,290..187,296] → 105,249 ops/sec [103,587..106,182] 🔴 -43.4% 170,328 ops/sec [164,989..171,206] → 91,504 ops/sec [91,232..91,889] 🔴 -46.3%
object parameter 95,424 ops/sec [95,191..95,944] → 53,856 ops/sec [53,223..54,455] 🔴 -43.6% 142,269 ops/sec [137,347..143,459] → 74,603 ops/sec [73,602..74,870] 🔴 -47.6%
array parameter 124,488 ops/sec [123,795..125,694] → 69,223 ops/sec [68,151..70,191] 🔴 -44.4% 126,555 ops/sec [124,845..127,940] → 66,818 ops/sec [66,437..67,530] 🔴 -47.2%
mixed destructuring in map 32,211 ops/sec [31,718..33,337] → 16,927 ops/sec [16,435..17,200] 🔴 -47.4% 43,715 ops/sec [42,650..44,082] → 20,666 ops/sec [20,298..20,993] 🔴 -52.7%
forEach with array destructuring 64,201 ops/sec [64,022..64,377] → 34,016 ops/sec [33,975..34,149] 🔴 -47.0% 47,708 ops/sec [47,432..48,859] → 25,873 ops/sec [24,728..25,970] 🔴 -45.8%
map with array destructuring 63,196 ops/sec [62,270..63,632] → 34,635 ops/sec [34,386..35,026] 🔴 -45.2% 43,549 ops/sec [40,882..44,741] → 23,838 ops/sec [23,676..23,949] 🔴 -45.3%
filter with array destructuring 65,496 ops/sec [64,683..66,415] → 36,373 ops/sec [36,273..36,405] 🔴 -44.5% 48,152 ops/sec [47,826..48,603] → 25,848 ops/sec [25,715..25,882] 🔴 -46.3%
reduce with array destructuring 72,390 ops/sec [72,027..73,122] → 39,553 ops/sec [39,490..39,749] 🔴 -45.4% 47,765 ops/sec [46,938..48,276] → 26,893 ops/sec [26,691..27,002] 🔴 -43.7%
map with object destructuring 72,686 ops/sec [70,331..73,328] → 37,453 ops/sec [37,201..38,890] 🔴 -48.5% 95,028 ops/sec [93,795..95,472] → 46,408 ops/sec [46,162..46,769] 🔴 -51.2%
map with nested destructuring 60,707 ops/sec [60,600..60,931] → 31,794 ops/sec [31,560..31,853] 🔴 -47.6% 88,204 ops/sec [87,730..89,270] → 42,379 ops/sec [41,794..42,806] 🔴 -52.0%
map with rest in destructuring 41,280 ops/sec [41,044..41,890] → 23,579 ops/sec [23,330..23,649] 🔴 -42.9% 25,772 ops/sec [25,711..25,848] → 13,988 ops/sec [13,535..14,157] 🔴 -45.7%
map with defaults in destructuring 55,705 ops/sec [55,458..55,784] → 30,167 ops/sec [29,510..30,619] 🔴 -45.8% 73,584 ops/sec [72,684..75,107] → 34,957 ops/sec [33,300..35,153] 🔴 -52.5%
fibonacci.js — Interp: 🔴 8 · avg -48.5% · Bytecode: 🔴 8 · avg -53.6%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 326 ops/sec [321..331] → 152 ops/sec [150..155] 🔴 -53.5% 1,432 ops/sec [1,430..1,436] → 583 ops/sec [564..590] 🔴 -59.3%
recursive fib(20) 28 ops/sec [28..29] → 14 ops/sec [14..14] 🔴 -50.8% 129 ops/sec [129..129] → 52 ops/sec [52..53] 🔴 -59.5%
recursive fib(15) typed 326 ops/sec [320..330] → 155 ops/sec [153..157] 🔴 -52.4% 1,090 ops/sec [1,085..1,097] → 468 ops/sec [461..480] 🔴 -57.1%
recursive fib(20) typed 28 ops/sec [28..28] → 14 ops/sec [13..14] 🔴 -51.6% 99 ops/sec [99..100] → 43 ops/sec [42..43] 🔴 -56.8%
iterative fib(20) via reduce 12,285 ops/sec [12,225..12,348] → 6,705 ops/sec [6,581..6,753] 🔴 -45.4% 19,002 ops/sec [18,704..19,178] → 10,688 ops/sec [10,629..10,819] 🔴 -43.8%
iterator fib(20) 9,498 ops/sec [9,332..9,521] → 5,404 ops/sec [5,236..5,452] 🔴 -43.1% 17,897 ops/sec [17,273..18,063] → 8,725 ops/sec [8,521..8,853] 🔴 -51.2%
iterator fib(20) via Iterator.from + take 15,515 ops/sec [15,427..15,551] → 8,505 ops/sec [8,444..8,527] 🔴 -45.2% 19,748 ops/sec [19,216..19,960] → 10,042 ops/sec [10,003..10,167] 🔴 -49.2%
iterator fib(20) last value via reduce 11,694 ops/sec [11,652..11,722] → 6,270 ops/sec [6,020..6,384] 🔴 -46.4% 15,094 ops/sec [14,987..15,261] → 7,217 ops/sec [6,973..7,308] 🔴 -52.2%
float16array.js — Interp: 🟢 2, 🔴 30 · avg -38.4% · Bytecode: 🔴 32 · avg -44.3%
Benchmark Interpreted Δ Bytecode Δ
new Float16Array(0) 344,632 ops/sec [339,613..347,478] → 178,627 ops/sec [177,346..180,749] 🔴 -48.2% 363,117 ops/sec [349,683..367,025] → 195,195 ops/sec [192,318..196,819] 🔴 -46.2%
new Float16Array(100) 316,695 ops/sec [312,583..321,127] → 169,342 ops/sec [165,686..170,846] 🔴 -46.5% 342,096 ops/sec [337,504..345,948] → 183,807 ops/sec [180,002..185,081] 🔴 -46.3%
new Float16Array(1000) 233,770 ops/sec [232,638..235,122] → 140,459 ops/sec [137,980..142,187] 🔴 -39.9% 248,989 ops/sec [238,648..250,628] → 143,021 ops/sec [140,195..144,099] 🔴 -42.6%
Float16Array.from([...100]) 167,565 ops/sec [160,929..172,792] → 98,165 ops/sec [97,264..99,589] 🔴 -41.4% 169,461 ops/sec [167,400..175,941] → 101,965 ops/sec [99,804..102,959] 🔴 -39.8%
Float16Array.of(1.5, 2.5, 3.5, 4.5, 5.5) 298,347 ops/sec [295,732..301,137] → 157,974 ops/sec [156,471..163,716] 🔴 -47.1% 239,231 ops/sec [236,303..241,687] → 126,140 ops/sec [124,330..127,207] 🔴 -47.3%
new Float16Array(float64Array) 201,838 ops/sec [201,456..203,368] → 106,084 ops/sec [104,646..107,622] 🔴 -47.4% 211,598 ops/sec [211,413..212,483] → 115,391 ops/sec [114,064..115,931] 🔴 -45.5%
sequential write 100 elements 3,240 ops/sec [3,227..3,252] → 1,816 ops/sec [1,795..1,852] 🔴 -44.0% 8,303 ops/sec [8,256..8,378] → 4,744 ops/sec [4,641..4,804] 🔴 -42.9%
sequential read 100 elements 3,510 ops/sec [3,503..3,515] → 1,989 ops/sec [1,974..2,003] 🔴 -43.3% 13,626 ops/sec [13,505..13,767] → 6,208 ops/sec [6,168..6,441] 🔴 -54.4%
write special values (NaN, Inf, -0) 205,668 ops/sec [203,652..206,630] → 99,128 ops/sec [98,208..99,685] 🔴 -51.8% 317,490 ops/sec [313,375..322,465] → 144,848 ops/sec [142,596..147,263] 🔴 -54.4%
Float16Array write 3,199 ops/sec [3,179..3,215] → 1,858 ops/sec [1,794..1,872] 🔴 -41.9% 8,341 ops/sec [8,241..8,404] → 4,722 ops/sec [4,667..4,745] 🔴 -43.4%
Float32Array write 3,258 ops/sec [3,231..3,264] → 1,859 ops/sec [1,848..1,863] 🔴 -42.9% 8,399 ops/sec [8,352..8,491] → 4,778 ops/sec [4,754..4,787] 🔴 -43.1%
Float64Array write 3,249 ops/sec [3,231..3,259] → 1,849 ops/sec [1,805..1,892] 🔴 -43.1% 8,345 ops/sec [8,312..8,409] → 4,789 ops/sec [4,762..4,887] 🔴 -42.6%
Float16Array read 3,314 ops/sec [3,305..3,337] → 1,926 ops/sec [1,896..1,986] 🔴 -41.9% 13,098 ops/sec [12,829..13,230] → 6,078 ops/sec [5,926..6,203] 🔴 -53.6%
Float32Array read 3,418 ops/sec [3,393..3,432] → 1,992 ops/sec [1,957..2,049] 🔴 -41.7% 14,572 ops/sec [14,530..14,694] → 6,526 ops/sec [6,443..6,591] 🔴 -55.2%
Float64Array read 3,421 ops/sec [3,387..3,435] → 2,016 ops/sec [1,992..2,021] 🔴 -41.1% 14,307 ops/sec [14,245..14,420] → 6,504 ops/sec [6,393..6,600] 🔴 -54.5%
fill(1.5) 39,688 ops/sec [39,630..39,761] → 24,360 ops/sec [23,746..24,737] 🔴 -38.6% 39,822 ops/sec [39,759..39,862] → 23,697 ops/sec [23,169..24,050] 🔴 -40.5%
slice() 146,676 ops/sec [145,390..147,110] → 96,202 ops/sec [94,466..96,304] 🔴 -34.4% 148,831 ops/sec [147,070..150,024] → 108,258 ops/sec [107,701..108,398] 🔴 -27.3%
map(x => x * 2) 7,370 ops/sec [7,293..7,407] → 3,724 ops/sec [3,704..3,729] 🔴 -49.5% 9,025 ops/sec [8,833..9,233] → 4,480 ops/sec [4,413..4,514] 🔴 -50.4%
filter(x => x > 25) 7,605 ops/sec [7,584..7,636] → 3,945 ops/sec [3,883..4,045] 🔴 -48.1% 10,048 ops/sec [9,947..10,139] → 4,880 ops/sec [4,860..4,905] 🔴 -51.4%
reduce (sum) 7,511 ops/sec [7,409..7,555] → 3,818 ops/sec [3,774..3,847] 🔴 -49.2% 8,263 ops/sec [8,042..8,508] → 3,920 ops/sec [3,919..3,928] 🔴 -52.6%
sort() 21,797 ops/sec [21,774..21,840] → 24,720 ops/sec [24,437..24,761] 🟢 +13.4% 27,801 ops/sec [27,737..27,863] → 21,955 ops/sec [21,870..22,037] 🔴 -21.0%
indexOf() 149,101 ops/sec [147,237..150,469] → 131,244 ops/sec [130,119..132,307] 🔴 -12.0% 193,497 ops/sec [193,080..193,864] → 135,076 ops/sec [134,378..135,786] 🔴 -30.2%
reverse() 182,357 ops/sec [181,549..183,741] → 124,576 ops/sec [123,765..125,207] 🔴 -31.7% 193,384 ops/sec [193,334..193,591] → 138,416 ops/sec [136,487..139,501] 🔴 -28.4%
toReversed() 67,295 ops/sec [67,014..68,401] → 63,165 ops/sec [62,581..63,559] 🔴 -6.1% 85,518 ops/sec [84,909..85,878] → 61,824 ops/sec [61,392..62,579] 🔴 -27.7%
toSorted() 802 ops/sec [802..804] → 964 ops/sec [951..967] 🟢 +20.1% 1,033 ops/sec [1,032..1,034] → 818 ops/sec [817..818] 🔴 -20.8%
create view over existing buffer 406,678 ops/sec [404,016..411,814] → 206,502 ops/sec [205,520..207,546] 🔴 -49.2% 454,155 ops/sec [451,380..454,585] → 234,720 ops/sec [232,907..234,860] 🔴 -48.3%
subarray() 431,794 ops/sec [430,243..432,817] → 225,986 ops/sec [223,455..229,052] 🔴 -47.7% 575,483 ops/sec [562,559..582,051] → 288,447 ops/sec [285,289..290,199] 🔴 -49.9%
set() from array 555,725 ops/sec [554,488..557,436] → 258,674 ops/sec [256,908..259,740] 🔴 -53.5% 612,822 ops/sec [611,275..613,119] → 303,659 ops/sec [302,023..305,479] 🔴 -50.4%
for-of loop 4,694 ops/sec [4,653..4,830] → 2,760 ops/sec [2,741..2,779] 🔴 -41.2% 19,479 ops/sec [19,077..19,699] → 8,999 ops/sec [8,912..9,112] 🔴 -53.8%
spread into array 16,803 ops/sec [16,643..16,846] → 10,160 ops/sec [10,078..10,198] 🔴 -39.5% 96,346 ops/sec [93,242..97,566] → 46,245 ops/sec [45,854..46,484] 🔴 -52.0%
f16round(1.337) 710,486 ops/sec [707,354..712,203] → 337,281 ops/sec [335,151..340,552] 🔴 -52.5% 662,956 ops/sec [661,098..664,760] → 322,989 ops/sec [321,164..325,766] 🔴 -51.3%
f16round over 100 values 3,486 ops/sec [3,368..3,573] → 1,900 ops/sec [1,891..1,908] 🔴 -45.5% 7,754 ops/sec [7,706..7,781] → 3,771 ops/sec [3,759..3,775] 🔴 -51.4%
for-of.js — Interp: 🔴 7 · avg -46.7% · Bytecode: 🔴 7 · avg -54.2%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 45,334 ops/sec [43,221..45,895] → 23,957 ops/sec [23,939..23,976] 🔴 -47.2% 281,455 ops/sec [275,936..283,659] → 114,785 ops/sec [113,230..118,149] 🔴 -59.2%
for...of with 100-element array 5,174 ops/sec [5,158..5,226] → 2,735 ops/sec [2,719..2,742] 🔴 -47.1% 35,238 ops/sec [34,322..35,736] → 13,834 ops/sec [13,711..14,005] 🔴 -60.7%
for...of with string (10 chars) 32,948 ops/sec [32,571..33,400] → 17,561 ops/sec [17,501..17,622] 🔴 -46.7% 81,257 ops/sec [79,344..83,995] → 39,912 ops/sec [39,059..40,390] 🔴 -50.9%
for...of with Set (10 elements) 45,366 ops/sec [44,551..45,555] → 24,067 ops/sec [23,894..24,099] 🔴 -47.0% 253,720 ops/sec [244,949..255,313] → 106,612 ops/sec [103,610..107,656] 🔴 -58.0%
for...of with Map entries (10 entries) 28,565 ops/sec [28,348..29,309] → 15,901 ops/sec [15,846..15,957] 🔴 -44.3% 32,035 ops/sec [31,373..32,129] → 17,348 ops/sec [17,001..17,661] 🔴 -45.8%
for...of with destructuring 38,473 ops/sec [38,336..38,846] → 20,307 ops/sec [20,117..20,481] 🔴 -47.2% 45,133 ops/sec [42,793..45,515] → 23,557 ops/sec [23,382..23,763] 🔴 -47.8%
for-await-of with sync array 43,639 ops/sec [42,653..43,761] → 23,077 ops/sec [22,868..23,172] 🔴 -47.1% 175,272 ops/sec [169,273..176,010] → 75,355 ops/sec [75,195..75,629] 🔴 -57.0%
helpers/bench-module.js — Interp: 0 · Bytecode: 0
Benchmark Interpreted Δ Bytecode Δ
iterators.js — Interp: 🔴 42 · avg -44.0% · Bytecode: 🔴 42 · avg -47.7%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 14,875 ops/sec [14,490..15,265] → 8,639 ops/sec [8,605..8,652] 🔴 -41.9% 20,150 ops/sec [20,044..20,304] → 10,198 ops/sec [10,039..10,286] 🔴 -49.4%
Iterator.from({next}).toArray() — 50 elements 6,534 ops/sec [6,531..6,555] → 3,727 ops/sec [3,619..3,763] 🔴 -43.0% 9,300 ops/sec [9,205..9,337] → 4,496 ops/sec [4,366..4,533] 🔴 -51.7%
spread pre-wrapped iterator — 20 elements 11,251 ops/sec [11,144..11,267] → 6,667 ops/sec [6,636..6,695] 🔴 -40.7% 19,937 ops/sec [19,734..20,017] → 10,028 ops/sec [9,854..10,060] 🔴 -49.7%
Iterator.from({next}).forEach — 50 elements 4,608 ops/sec [4,595..4,617] → 2,493 ops/sec [2,459..2,527] 🔴 -45.9% 6,389 ops/sec [6,357..6,396] → 3,070 ops/sec [3,001..3,092] 🔴 -52.0%
Iterator.from({next}).reduce — 50 elements 4,567 ops/sec [4,519..4,623] → 2,570 ops/sec [2,565..2,579] 🔴 -43.7% 6,120 ops/sec [5,994..6,158] → 3,071 ops/sec [2,967..3,097] 🔴 -49.8%
wrap array iterator 164,611 ops/sec [161,427..164,775] → 89,871 ops/sec [89,444..90,090] 🔴 -45.4% 181,835 ops/sec [179,726..182,302] → 91,339 ops/sec [90,014..92,310] 🔴 -49.8%
wrap plain {next()} object 10,523 ops/sec [10,415..10,577] → 6,080 ops/sec [6,033..6,122] 🔴 -42.2% 14,182 ops/sec [13,996..14,267] → 7,163 ops/sec [7,013..7,284] 🔴 -49.5%
map + toArray (50 elements) 4,606 ops/sec [4,559..4,621] → 2,560 ops/sec [2,553..2,565] 🔴 -44.4% 6,457 ops/sec [6,263..6,539] → 3,027 ops/sec [3,001..3,062] 🔴 -53.1%
filter + toArray (50 elements) 4,411 ops/sec [4,366..4,464] → 2,524 ops/sec [2,506..2,537] 🔴 -42.8% 6,241 ops/sec [6,186..6,273] → 3,071 ops/sec [3,056..3,094] 🔴 -50.8%
take(10) + toArray (50 element source) 27,571 ops/sec [27,478..27,690] → 15,999 ops/sec [15,877..16,051] 🔴 -42.0% 36,548 ops/sec [36,314..36,707] → 18,182 ops/sec [17,917..18,258] 🔴 -50.3%
drop(40) + toArray (50 element source) 6,616 ops/sec [6,512..6,671] → 3,773 ops/sec [3,702..3,786] 🔴 -43.0% 9,107 ops/sec [9,050..9,141] → 4,478 ops/sec [4,427..4,545] 🔴 -50.8%
chained map + filter + take (100 element source) 8,330 ops/sec [8,300..8,357] → 4,613 ops/sec [4,525..4,634] 🔴 -44.6% 11,208 ops/sec [11,038..11,307] → 5,409 ops/sec [5,330..5,476] 🔴 -51.7%
some + every (50 elements) 2,624 ops/sec [2,581..2,660] → 1,475 ops/sec [1,460..1,482] 🔴 -43.8% 3,689 ops/sec [3,544..3,701] → 1,767 ops/sec [1,751..1,780] 🔴 -52.1%
find (50 elements) 5,809 ops/sec [5,798..5,816] → 3,207 ops/sec [3,181..3,215] 🔴 -44.8% 7,976 ops/sec [7,781..8,014] → 3,861 ops/sec [3,714..3,911] 🔴 -51.6%
concat 2 arrays (10 + 10 elements) 158,774 ops/sec [158,193..160,451] → 84,961 ops/sec [84,396..85,283] 🔴 -46.5% 173,216 ops/sec [170,571..174,049] → 84,054 ops/sec [83,287..84,167] 🔴 -51.5%
concat 5 arrays (10 elements each) 96,325 ops/sec [95,183..96,890] → 51,796 ops/sec [51,205..52,241] 🔴 -46.2% 105,501 ops/sec [105,010..105,872] → 51,931 ops/sec [50,972..52,622] 🔴 -50.8%
concat 2 arrays (20 + 20 elements) 142,426 ops/sec [141,892..143,284] → 77,113 ops/sec [76,508..77,538] 🔴 -45.9% 157,056 ops/sec [153,985..157,704] → 76,886 ops/sec [74,705..77,977] 🔴 -51.0%
concat + filter + toArray (20 + 20 elements) 16,885 ops/sec [16,683..16,997] → 8,564 ops/sec [8,465..8,661] 🔴 -49.3% 21,877 ops/sec [21,584..21,948] → 10,655 ops/sec [10,603..10,689] 🔴 -51.3%
concat + map + take (20 + 20 elements, take 10) 50,269 ops/sec [49,113..50,406] → 25,754 ops/sec [25,679..25,950] 🔴 -48.8% 60,810 ops/sec [60,594..61,011] → 29,177 ops/sec [29,008..29,945] 🔴 -52.0%
concat Sets (15 + 15 elements) 142,369 ops/sec [141,997..142,917] → 79,440 ops/sec [78,724..80,237] 🔴 -44.2% 151,870 ops/sec [149,841..154,830] → 74,658 ops/sec [73,302..75,800] 🔴 -50.8%
concat strings (13 + 13 characters) 106,291 ops/sec [105,816..106,724] → 56,580 ops/sec [56,115..57,233] 🔴 -46.8% 103,761 ops/sec [96,727..105,446] → 53,453 ops/sec [52,602..54,134] 🔴 -48.5%
zip 2 arrays (10 + 10 elements) 55,123 ops/sec [54,385..56,584] → 32,118 ops/sec [31,952..32,307] 🔴 -41.7% 55,007 ops/sec [53,149..56,090] → 30,728 ops/sec [30,494..31,392] 🔴 -44.1%
zip 3 arrays (10 elements each) 52,415 ops/sec [51,656..53,156] → 29,458 ops/sec [29,343..29,690] 🔴 -43.8% 52,091 ops/sec [50,458..52,428] → 28,357 ops/sec [27,936..28,685] 🔴 -45.6%
zip 2 arrays (20 + 20 elements) 37,053 ops/sec [36,855..37,598] → 21,321 ops/sec [21,020..21,356] 🔴 -42.5% 37,418 ops/sec [36,101..38,481] → 20,430 ops/sec [19,817..20,825] 🔴 -45.4%
zip 2 arrays (50 + 50 elements) 19,145 ops/sec [19,111..19,224] → 10,795 ops/sec [10,732..10,884] 🔴 -43.6% 18,425 ops/sec [17,269..18,611] → 10,464 ops/sec [10,426..10,586] 🔴 -43.2%
zip shortest mode (20 + 10 elements) 56,112 ops/sec [55,808..56,805] → 32,236 ops/sec [32,064..32,541] 🔴 -42.6% 55,440 ops/sec [52,087..56,418] → 32,557 ops/sec [31,701..32,834] 🔴 -41.3%
zip longest mode (10 + 20 elements) 33,104 ops/sec [32,943..33,539] → 19,054 ops/sec [18,935..19,134] 🔴 -42.4% 32,028 ops/sec [31,545..32,133] → 18,874 ops/sec [18,764..19,198] 🔴 -41.1%
zip strict mode (20 + 20 elements) 35,239 ops/sec [35,123..35,861] → 20,495 ops/sec [19,960..20,692] 🔴 -41.8% 35,114 ops/sec [33,584..35,855] → 20,439 ops/sec [20,257..20,504] 🔴 -41.8%
zip + map + toArray (20 + 20 elements) 17,516 ops/sec [17,427..17,624] → 9,723 ops/sec [9,639..9,792] 🔴 -44.5% 11,660 ops/sec [11,169..11,795] → 6,693 ops/sec [6,687..6,732] 🔴 -42.6%
zip + filter + toArray (20 + 20 elements) 16,648 ops/sec [16,630..16,711] → 9,471 ops/sec [9,302..9,504] 🔴 -43.1% 11,583 ops/sec [11,385..11,621] → 6,866 ops/sec [6,833..6,902] 🔴 -40.7%
zip Sets (15 + 15 elements) 44,742 ops/sec [44,016..45,440] → 25,752 ops/sec [25,277..26,131] 🔴 -42.4% 44,034 ops/sec [43,892..44,442] → 25,793 ops/sec [25,660..25,901] 🔴 -41.4%
zipKeyed 2 keys (10 elements each) 52,887 ops/sec [51,878..53,496] → 32,018 ops/sec [31,895..32,245] 🔴 -39.5% 54,422 ops/sec [53,123..55,310] → 31,733 ops/sec [31,364..32,506] 🔴 -41.7%
zipKeyed 3 keys (20 elements each) 27,051 ops/sec [26,898..27,159] → 16,238 ops/sec [16,116..16,286] 🔴 -40.0% 27,915 ops/sec [26,281..28,261] → 15,606 ops/sec [15,452..15,830] 🔴 -44.1%
zipKeyed longest mode (10 + 20 elements) 30,456 ops/sec [30,302..30,620] → 18,541 ops/sec [18,336..18,697] 🔴 -39.1% 31,267 ops/sec [30,628..31,689] → 17,563 ops/sec [17,286..17,754] 🔴 -43.8%
zipKeyed strict mode (20 + 20 elements) 31,839 ops/sec [31,528..32,126] → 19,682 ops/sec [19,345..19,825] 🔴 -38.2% 31,795 ops/sec [30,745..32,186] → 18,958 ops/sec [18,765..19,219] 🔴 -40.4%
zipKeyed + filter + map (20 elements) 12,759 ops/sec [12,670..12,775] → 7,191 ops/sec [7,139..7,240] 🔴 -43.6% 14,545 ops/sec [14,110..14,801] → 7,812 ops/sec [7,642..7,922] 🔴 -46.3%
array.values().map().filter().toArray() 8,313 ops/sec [8,228..8,466] → 4,091 ops/sec [4,084..4,099] 🔴 -50.8% 11,133 ops/sec [11,089..11,228] → 5,065 ops/sec [5,002..5,170] 🔴 -54.5%
array.values().take(5).toArray() 212,305 ops/sec [210,681..214,472] → 112,623 ops/sec [111,092..114,353] 🔴 -47.0% 239,045 ops/sec [230,339..241,364] → 127,505 ops/sec [126,199..128,172] 🔴 -46.7%
array.values().drop(45).toArray() 196,881 ops/sec [196,287..197,775] → 107,713 ops/sec [107,406..108,103] 🔴 -45.3% 218,890 ops/sec [209,542..222,437] → 117,089 ops/sec [116,277..118,924] 🔴 -46.5%
map.entries() chained helpers 10,291 ops/sec [10,175..10,362] → 5,527 ops/sec [5,489..5,599] 🔴 -46.3% 6,252 ops/sec [6,144..6,353] → 3,427 ops/sec [3,386..3,440] 🔴 -45.2%
set.values() chained helpers 17,664 ops/sec [17,463..17,821] → 9,039 ops/sec [9,007..9,057] 🔴 -48.8% 22,514 ops/sec [22,445..22,750] → 11,379 ops/sec [11,315..11,479] 🔴 -49.5%
string iterator map + toArray 13,854 ops/sec [13,780..13,935] → 7,346 ops/sec [7,304..7,434] 🔴 -47.0% 14,754 ops/sec [14,209..14,891] → 7,601 ops/sec [7,280..7,655] 🔴 -48.5%
json.js — Interp: 🔴 20 · avg -44.5% · Bytecode: 🔴 20 · avg -47.2%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 148,755 ops/sec [147,379..150,372] → 79,054 ops/sec [77,275..80,386] 🔴 -46.9% 148,887 ops/sec [144,591..150,286] → 77,891 ops/sec [76,791..78,191] 🔴 -47.7%
parse nested object 92,407 ops/sec [91,635..93,781] → 52,553 ops/sec [52,312..53,098] 🔴 -43.1% 93,354 ops/sec [92,081..93,849] → 52,135 ops/sec [51,237..52,317] 🔴 -44.2%
parse array of objects 54,114 ops/sec [53,763..54,487] → 31,881 ops/sec [31,569..31,917] 🔴 -41.1% 55,837 ops/sec [53,087..56,109] → 30,779 ops/sec [30,461..30,829] 🔴 -44.9%
parse large flat object 61,741 ops/sec [61,492..61,869] → 35,239 ops/sec [34,726..36,339] 🔴 -42.9% 61,869 ops/sec [61,514..62,095] → 31,920 ops/sec [31,591..32,565] 🔴 -48.4%
parse mixed types 67,523 ops/sec [67,363..67,586] → 40,262 ops/sec [40,231..40,335] 🔴 -40.4% 69,325 ops/sec [68,268..69,807] → 37,327 ops/sec [37,040..37,515] 🔴 -46.2%
stringify simple object 160,078 ops/sec [158,965..161,292] → 89,643 ops/sec [89,129..89,742] 🔴 -44.0% 159,900 ops/sec [158,709..163,188] → 81,221 ops/sec [80,102..82,356] 🔴 -49.2%
stringify nested object 88,928 ops/sec [88,712..89,042] → 50,052 ops/sec [49,650..50,190] 🔴 -43.7% 86,632 ops/sec [85,885..88,000] → 44,848 ops/sec [43,906..45,454] 🔴 -48.2%
stringify array of objects 39,339 ops/sec [39,269..39,402] → 20,399 ops/sec [20,284..20,723] 🔴 -48.1% 39,385 ops/sec [39,203..39,538] → 19,238 ops/sec [19,016..19,398] 🔴 -51.2%
stringify mixed types 66,765 ops/sec [66,365..66,970] → 36,246 ops/sec [35,791..38,959] 🔴 -45.7% 63,691 ops/sec [63,305..64,277] → 33,717 ops/sec [33,403..34,278] 🔴 -47.1%
reviver doubles numbers 33,272 ops/sec [33,238..33,454] → 18,753 ops/sec [18,510..18,993] 🔴 -43.6% 35,265 ops/sec [34,756..36,742] → 18,939 ops/sec [18,622..19,145] 🔴 -46.3%
reviver filters properties 30,628 ops/sec [30,508..30,691] → 17,204 ops/sec [17,119..17,431] 🔴 -43.8% 30,399 ops/sec [29,694..30,873] → 17,007 ops/sec [16,734..17,184] 🔴 -44.1%
reviver on nested object 37,316 ops/sec [36,916..37,633] → 21,260 ops/sec [21,162..21,373] 🔴 -43.0% 38,669 ops/sec [38,285..39,097] → 21,264 ops/sec [21,052..21,525] 🔴 -45.0%
reviver on array 20,800 ops/sec [20,672..20,837] → 11,619 ops/sec [11,411..11,665] 🔴 -44.1% 22,761 ops/sec [22,478..23,062] → 11,881 ops/sec [10,764..12,078] 🔴 -47.8%
replacer function doubles numbers 41,316 ops/sec [40,433..41,596] → 22,277 ops/sec [22,227..22,637] 🔴 -46.1% 44,461 ops/sec [42,708..45,291] → 22,947 ops/sec [22,766..23,105] 🔴 -48.4%
replacer function excludes properties 54,364 ops/sec [54,093..54,576] → 29,490 ops/sec [28,941..29,747] 🔴 -45.8% 54,464 ops/sec [53,819..54,782] → 28,055 ops/sec [27,637..28,594] 🔴 -48.5%
array replacer (allowlist) 95,227 ops/sec [94,902..95,784] → 52,075 ops/sec [51,954..52,626] 🔴 -45.3% 93,865 ops/sec [91,995..94,815] → 48,636 ops/sec [47,801..49,485] 🔴 -48.2%
stringify with 2-space indent 76,914 ops/sec [76,610..77,138] → 41,501 ops/sec [40,945..41,750] 🔴 -46.0% 76,459 ops/sec [75,787..77,245] → 40,530 ops/sec [40,273..40,886] 🔴 -47.0%
stringify with tab indent 76,766 ops/sec [76,320..77,204] → 41,724 ops/sec [41,137..42,109] 🔴 -45.6% 74,744 ops/sec [73,910..75,087] → 39,765 ops/sec [39,352..40,419] 🔴 -46.8%
parse then stringify 45,202 ops/sec [45,073..45,420] → 24,473 ops/sec [24,381..24,837] 🔴 -45.9% 46,576 ops/sec [45,216..47,900] → 24,444 ops/sec [23,866..24,715] 🔴 -47.5%
stringify then parse 26,372 ops/sec [26,262..26,451] → 14,651 ops/sec [14,509..14,677] 🔴 -44.4% 26,755 ops/sec [26,324..27,432] → 14,261 ops/sec [13,946..14,456] 🔴 -46.7%
jsx.jsx — Interp: 🔴 21 · avg -46.4% · Bytecode: 🔴 21 · avg -50.8%
Benchmark Interpreted Δ Bytecode Δ
simple element 213,415 ops/sec [211,751..214,362] → 113,781 ops/sec [112,337..114,044] 🔴 -46.7% 312,519 ops/sec [292,944..313,300] → 155,205 ops/sec [153,855..157,198] 🔴 -50.3%
self-closing element 225,243 ops/sec [222,962..226,387] → 116,384 ops/sec [115,761..117,549] 🔴 -48.3% 348,796 ops/sec [336,502..352,275] → 175,574 ops/sec [175,134..181,366] 🔴 -49.7%
element with string attribute 181,884 ops/sec [181,474..182,353] → 95,585 ops/sec [95,154..95,975] 🔴 -47.4% 245,741 ops/sec [241,896..248,367] → 119,816 ops/sec [118,124..120,197] 🔴 -51.2%
element with multiple attributes 160,598 ops/sec [160,076..161,122] → 83,145 ops/sec [82,258..83,559] 🔴 -48.2% 189,865 ops/sec [188,130..190,268] → 93,636 ops/sec [92,699..93,930] 🔴 -50.7%
element with expression attribute 172,081 ops/sec [170,750..172,543] → 91,817 ops/sec [90,903..92,534] 🔴 -46.6% 249,961 ops/sec [237,753..253,104] → 121,104 ops/sec [120,149..121,443] 🔴 -51.6%
text child 215,379 ops/sec [214,437..217,340] → 112,892 ops/sec [111,886..113,230] 🔴 -47.6% 317,002 ops/sec [308,832..320,977] → 158,685 ops/sec [157,996..159,336] 🔴 -49.9%
expression child 208,992 ops/sec [208,257..209,232] → 111,693 ops/sec [111,366..111,912] 🔴 -46.6% 313,556 ops/sec [303,223..318,595] → 153,485 ops/sec [152,020..155,671] 🔴 -51.1%
mixed text and expression 198,284 ops/sec [197,454..198,958] → 105,071 ops/sec [103,596..105,616] 🔴 -47.0% 271,286 ops/sec [269,502..272,063] → 134,451 ops/sec [133,569..135,375] 🔴 -50.4%
nested elements (3 levels) 81,578 ops/sec [81,084..81,659] → 43,076 ops/sec [42,231..44,172] 🔴 -47.2% 119,292 ops/sec [118,125..120,249] → 57,248 ops/sec [56,799..57,914] 🔴 -52.0%
sibling children 60,038 ops/sec [59,817..60,381] → 31,948 ops/sec [31,612..32,160] 🔴 -46.8% 85,947 ops/sec [84,206..86,631] → 40,998 ops/sec [40,585..41,207] 🔴 -52.3%
component element 151,647 ops/sec [150,868..152,430] → 84,032 ops/sec [82,949..84,979] 🔴 -44.6% 220,660 ops/sec [217,237..222,605] → 105,106 ops/sec [104,386..107,487] 🔴 -52.4%
component with children 93,165 ops/sec [92,911..93,596] → 51,016 ops/sec [50,229..52,816] 🔴 -45.2% 132,451 ops/sec [130,652..133,759] → 64,942 ops/sec [63,045..65,538] 🔴 -51.0%
dotted component 126,586 ops/sec [126,287..126,885] → 71,353 ops/sec [69,930..71,763] 🔴 -43.6% 169,573 ops/sec [166,741..170,566] → 84,087 ops/sec [82,688..84,523] 🔴 -50.4%
empty fragment 227,635 ops/sec [227,195..228,090] → 118,913 ops/sec [118,649..120,028] 🔴 -47.8% 384,052 ops/sec [358,974..391,554] → 181,721 ops/sec [177,415..183,206] 🔴 -52.7%
fragment with children 59,780 ops/sec [59,632..59,947] → 32,176 ops/sec [31,947..32,417] 🔴 -46.2% 86,455 ops/sec [84,849..87,797] → 42,326 ops/sec [42,104..42,866] 🔴 -51.0%
spread attributes 110,701 ops/sec [110,554..110,840] → 58,958 ops/sec [58,631..59,123] 🔴 -46.7% 126,361 ops/sec [124,796..126,906] → 63,351 ops/sec [62,936..63,654] 🔴 -49.9%
spread with overrides 98,512 ops/sec [97,950..98,642] → 52,694 ops/sec [51,912..52,922] 🔴 -46.5% 110,026 ops/sec [109,850..111,171] → 55,179 ops/sec [54,966..56,230] 🔴 -49.8%
shorthand props 163,020 ops/sec [162,725..163,688] → 88,581 ops/sec [87,734..89,897] 🔴 -45.7% 229,558 ops/sec [227,469..231,468] → 110,628 ops/sec [108,957..112,106] 🔴 -51.8%
nav bar structure 27,833 ops/sec [27,708..27,902] → 15,456 ops/sec [15,398..15,486] 🔴 -44.5% 38,817 ops/sec [37,848..39,036] → 19,062 ops/sec [18,842..19,203] 🔴 -50.9%
card component tree 32,536 ops/sec [32,483..32,571] → 17,895 ops/sec [17,722..18,112] 🔴 -45.0% 42,542 ops/sec [42,494..42,820] → 21,361 ops/sec [21,113..21,520] 🔴 -49.8%
10 list items via Array.from 14,825 ops/sec [14,738..14,891] → 8,128 ops/sec [8,018..8,152] 🔴 -45.2% 18,413 ops/sec [18,389..18,473] → 9,457 ops/sec [9,304..9,754] 🔴 -48.6%
modules.js — Interp: 🔴 9 · avg -55.1% · Bytecode: 🔴 9 · avg -57.8%
Benchmark Interpreted Δ Bytecode Δ
call imported function 468,266 ops/sec [464,228..471,328] → 216,911 ops/sec [214,187..219,276] 🔴 -53.7% 680,717 ops/sec [675,568..682,586] → 297,760 ops/sec [292,780..302,387] 🔴 -56.3%
call two imported functions 267,615 ops/sec [265,910..268,932] → 123,725 ops/sec [123,570..124,252] 🔴 -53.8% 355,704 ops/sec [351,792..357,581] → 162,371 ops/sec [157,830..164,552] 🔴 -54.4%
read imported constant 1,691,345 ops/sec [1,679,433..1,701,457] → 725,199 ops/sec [715,894..732,210] 🔴 -57.1% 5,723,812 ops/sec [5,710,948..5,734,919] → 2,356,758 ops/sec [2,331,122..2,359,065] 🔴 -58.8%
read imported string 1,619,624 ops/sec [1,614,312..1,632,443] → 725,121 ops/sec [719,978..727,744] 🔴 -55.2% 5,686,946 ops/sec [5,681,356..5,716,195] → 2,352,431 ops/sec [2,320,425..2,368,564] 🔴 -58.6%
read JSON string property 1,629,392 ops/sec [1,624,199..1,638,234] → 733,919 ops/sec [725,799..735,602] 🔴 -55.0% 5,680,565 ops/sec [5,667,628..5,692,971] → 2,380,083 ops/sec [2,339,892..2,393,416] 🔴 -58.1%
read JSON number property 1,609,319 ops/sec [1,600,244..1,611,623] → 722,753 ops/sec [713,329..725,312] 🔴 -55.1% 5,726,499 ops/sec [5,705,824..5,735,497] → 2,352,084 ops/sec [2,330,927..2,369,381] 🔴 -58.9%
read JSON boolean property 1,628,650 ops/sec [1,626,978..1,630,483] → 727,945 ops/sec [724,940..729,504] 🔴 -55.3% 5,682,858 ops/sec [5,661,916..5,698,375] → 2,351,195 ops/sec [2,329,474..2,378,496] 🔴 -58.6%
read JSON array property 1,621,565 ops/sec [1,611,037..1,631,476] → 726,064 ops/sec [720,122..728,512] 🔴 -55.2% 5,681,815 ops/sec [5,677,588..5,690,016] → 2,334,205 ops/sec [2,318,721..2,344,147] 🔴 -58.9%
read multiple JSON properties 908,950 ops/sec [906,861..911,068] → 402,103 ops/sec [400,409..402,492] 🔴 -55.8% 4,710,430 ops/sec [4,701,880..4,716,442] → 1,982,793 ops/sec [1,955,331..1,986,979] 🔴 -57.9%
numbers.js — Interp: 🔴 11 · avg -53.1% · Bytecode: 🔴 11 · avg -49.6%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 518,341 ops/sec [506,766..520,216] → 234,817 ops/sec [231,415..236,335] 🔴 -54.7% 1,642,334 ops/sec [1,586,127..1,689,808] → 882,540 ops/sec [870,057..886,039] 🔴 -46.3%
floating point arithmetic 637,266 ops/sec [611,939..644,150] → 273,632 ops/sec [272,166..279,019] 🔴 -57.1% 930,177 ops/sec [873,853..937,268] → 491,306 ops/sec [488,091..496,370] 🔴 -47.2%
number coercion 187,578 ops/sec [185,451..187,993] → 82,018 ops/sec [81,356..82,344] 🔴 -56.3% 206,808 ops/sec [206,173..207,945] → 102,317 ops/sec [101,653..102,595] 🔴 -50.5%
toFixed 102,429 ops/sec [101,772..102,847] → 54,828 ops/sec [54,527..55,010] 🔴 -46.5% 108,764 ops/sec [107,074..109,786] → 59,512 ops/sec [59,407..60,047] 🔴 -45.3%
toString 152,742 ops/sec [152,391..153,039] → 80,745 ops/sec [78,339..82,458] 🔴 -47.1% 178,090 ops/sec [174,933..178,830] → 90,883 ops/sec [89,291..92,646] 🔴 -49.0%
valueOf 229,057 ops/sec [224,532..230,680] → 117,314 ops/sec [116,873..117,853] 🔴 -48.8% 263,502 ops/sec [261,263..265,078] → 126,071 ops/sec [125,017..126,693] 🔴 -52.2%
toPrecision 144,363 ops/sec [144,284..144,724] → 75,301 ops/sec [74,758..75,430] 🔴 -47.8% 157,841 ops/sec [156,333..158,649] → 81,206 ops/sec [81,108..81,273] 🔴 -48.6%
Number.isNaN 326,972 ops/sec [326,603..329,235] → 140,504 ops/sec [137,228..143,350] 🔴 -57.0% 300,006 ops/sec [297,371..301,041] → 153,884 ops/sec [144,160..155,472] 🔴 -48.7%
Number.isFinite 313,833 ops/sec [312,783..314,846] → 134,683 ops/sec [133,413..140,099] 🔴 -57.1% 291,349 ops/sec [286,884..292,550] → 134,409 ops/sec [131,704..137,836] 🔴 -53.9%
Number.isInteger 328,836 ops/sec [326,488..329,795] → 142,724 ops/sec [141,543..150,899] 🔴 -56.6% 309,788 ops/sec [308,293..312,089] → 146,286 ops/sec [146,258..146,505] 🔴 -52.8%
Number.parseInt and parseFloat 257,254 ops/sec [253,770..259,562] → 114,587 ops/sec [114,003..117,473] 🔴 -55.5% 234,392 ops/sec [232,216..234,821] → 113,932 ops/sec [113,321..117,064] 🔴 -51.4%
objects.js — Interp: 🔴 7 · avg -49.3% · Bytecode: 🔴 7 · avg -50.1%
Benchmark Interpreted Δ Bytecode Δ
create simple object 485,423 ops/sec [483,701..494,299] → 250,734 ops/sec [243,113..252,444] 🔴 -48.3% 572,773 ops/sec [551,490..575,722] → 276,254 ops/sec [271,510..282,481] 🔴 -51.8%
create nested object 238,233 ops/sec [237,111..239,430] → 128,150 ops/sec [127,356..129,350] 🔴 -46.2% 231,876 ops/sec [223,596..235,731] → 121,538 ops/sec [110,041..123,078] 🔴 -47.6%
create 50 objects via Array.from 9,191 ops/sec [9,176..9,259] → 5,015 ops/sec [4,966..5,036] 🔴 -45.4% 9,530 ops/sec [9,507..9,550] → 4,831 ops/sec [4,745..4,864] 🔴 -49.3%
property read 564,661 ops/sec [563,080..581,494] → 249,415 ops/sec [244,283..251,772] 🔴 -55.8% 753,059 ops/sec [747,655..766,514] → 358,702 ops/sec [353,776..370,272] 🔴 -52.4%
Object.keys 284,320 ops/sec [281,035..285,139] → 129,778 ops/sec [127,308..131,021] 🔴 -54.4% 302,069 ops/sec [297,552..303,878] → 149,287 ops/sec [146,337..152,184] 🔴 -50.6%
Object.entries 99,566 ops/sec [98,992..99,905] → 51,404 ops/sec [50,741..52,202] 🔴 -48.4% 108,590 ops/sec [107,460..109,221] → 54,764 ops/sec [54,314..55,117] 🔴 -49.6%
spread operator 180,243 ops/sec [179,607..181,244] → 95,988 ops/sec [94,406..96,809] 🔴 -46.7% 202,824 ops/sec [191,988..204,326] → 102,733 ops/sec [101,917..103,226] 🔴 -49.3%
promises.js — Interp: 🔴 12 · avg -48.5% · Bytecode: 🔴 12 · avg -51.2%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 534,525 ops/sec [533,949..534,826] → 254,165 ops/sec [249,480..254,747] 🔴 -52.5% 559,331 ops/sec [517,522..561,054] → 273,459 ops/sec [266,648..277,842] 🔴 -51.1%
new Promise(resolve => resolve(value)) 188,856 ops/sec [187,428..190,165] → 100,049 ops/sec [95,648..100,376] 🔴 -47.0% 230,532 ops/sec [225,452..233,372] → 110,434 ops/sec [110,321..110,600] 🔴 -52.1%
Promise.reject(reason) 548,376 ops/sec [542,340..549,529] → 270,087 ops/sec [269,752..270,144] 🔴 -50.7% 527,444 ops/sec [518,890..530,587] → 262,740 ops/sec [260,157..264,385] 🔴 -50.2%
resolve + then (1 handler) 183,601 ops/sec [183,391..183,819] → 94,187 ops/sec [93,878..94,814] 🔴 -48.7% 212,671 ops/sec [208,484..214,612] → 104,909 ops/sec [103,634..106,705] 🔴 -50.7%
resolve + then chain (3 deep) 71,959 ops/sec [71,639..72,276] → 37,908 ops/sec [37,211..39,397] 🔴 -47.3% 86,719 ops/sec [85,977..86,819] → 42,384 ops/sec [42,039..43,212] 🔴 -51.1%
resolve + then chain (10 deep) 23,091 ops/sec [23,038..23,138] → 12,192 ops/sec [12,120..12,381] 🔴 -47.2% 28,302 ops/sec [28,059..28,517] → 14,037 ops/sec [13,986..14,133] 🔴 -50.4%
reject + catch + then 103,010 ops/sec [102,666..103,401] → 51,675 ops/sec [50,920..53,110] 🔴 -49.8% 113,858 ops/sec [109,924..114,214] → 55,906 ops/sec [55,060..56,706] 🔴 -50.9%
resolve + finally + then 86,436 ops/sec [86,113..86,749] → 44,162 ops/sec [43,232..44,728] 🔴 -48.9% 95,355 ops/sec [95,088..96,197] → 45,330 ops/sec [45,179..46,015] 🔴 -52.5%
Promise.all (5 resolved) 31,906 ops/sec [31,849..31,959] → 16,817 ops/sec [16,531..17,060] 🔴 -47.3% 32,611 ops/sec [32,195..33,014] → 15,630 ops/sec [15,293..15,658] 🔴 -52.1%
Promise.race (5 resolved) 33,777 ops/sec [33,776..33,804] → 17,607 ops/sec [17,538..17,690] 🔴 -47.9% 34,561 ops/sec [34,497..34,777] → 16,863 ops/sec [16,453..17,025] 🔴 -51.2%
Promise.allSettled (5 mixed) 26,727 ops/sec [26,692..26,825] → 14,215 ops/sec [14,166..14,271] 🔴 -46.8% 27,686 ops/sec [27,310..27,780] → 13,490 ops/sec [13,095..13,848] 🔴 -51.3%
Promise.any (5 mixed) 32,013 ops/sec [31,916..32,048] → 16,713 ops/sec [16,583..16,849] 🔴 -47.8% 32,487 ops/sec [31,511..32,809] → 15,910 ops/sec [15,741..16,215] 🔴 -51.0%
regexp.js — Interp: 🔴 11 · avg -47.6% · Bytecode: 🔴 11 · avg -47.3%
Benchmark Interpreted Δ Bytecode Δ
regex literal creation 143,716 ops/sec [142,766..144,464] → 71,942 ops/sec [70,965..72,532] 🔴 -49.9% 123,311 ops/sec [123,065..126,376] → 65,986 ops/sec [65,353..66,560] 🔴 -46.5%
new RegExp(pattern, flags) 121,246 ops/sec [121,024..121,659] → 63,548 ops/sec [62,938..64,138] 🔴 -47.6% 125,916 ops/sec [123,117..127,111] → 64,752 ops/sec [64,287..65,001] 🔴 -48.6%
RegExp(existingRegex) returns the same regex 727,100 ops/sec [720,952..730,858] → 319,205 ops/sec [317,730..320,091] 🔴 -56.1% 1,009,989 ops/sec [1,005,349..1,011,555] → 459,243 ops/sec [455,773..461,539] 🔴 -54.5%
test() on a global regex 133,082 ops/sec [132,976..133,243] → 70,111 ops/sec [69,351..71,207] 🔴 -47.3% 149,078 ops/sec [148,318..150,553] → 81,578 ops/sec [80,844..81,893] 🔴 -45.3%
exec() with capture groups 110,575 ops/sec [109,584..111,118] → 61,676 ops/sec [59,540..62,140] 🔴 -44.2% 121,482 ops/sec [117,726..123,142] → 65,534 ops/sec [65,323..65,810] 🔴 -46.1%
toString() 497,617 ops/sec [494,506..499,831] → 235,391 ops/sec [234,445..236,318] 🔴 -52.7% 644,968 ops/sec [640,274..648,634] → 294,083 ops/sec [292,413..294,477] 🔴 -54.4%
match() with global regex 38,012 ops/sec [37,923..38,303] → 20,781 ops/sec [20,419..20,817] 🔴 -45.3% 35,690 ops/sec [35,578..36,354] → 19,515 ops/sec [19,362..19,850] 🔴 -45.3%
matchAll() with capture groups 19,411 ops/sec [19,241..19,569] → 10,541 ops/sec [10,319..10,734] 🔴 -45.7% 22,560 ops/sec [22,405..22,663] → 12,287 ops/sec [12,003..12,430] 🔴 -45.5%
replace() with global regex 35,104 ops/sec [34,919..35,172] → 19,364 ops/sec [19,183..19,491] 🔴 -44.8% 34,460 ops/sec [33,826..35,186] → 19,220 ops/sec [18,898..19,527] 🔴 -44.2%
search() with regex 72,060 ops/sec [71,825..72,308] → 39,719 ops/sec [38,528..40,152] 🔴 -44.9% 69,888 ops/sec [69,219..70,166] → 38,252 ops/sec [37,838..38,602] 🔴 -45.3%
split() with regex separator 35,800 ops/sec [35,693..35,924] → 19,866 ops/sec [19,298..20,051] 🔴 -44.5% 35,812 ops/sec [35,198..35,874] → 19,843 ops/sec [19,802..19,886] 🔴 -44.6%
strings.js — Interp: 🔴 19 · avg -43.2% · Bytecode: 🔴 19 · avg -34.0%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 394,978 ops/sec [391,715..397,808] → 200,757 ops/sec [199,474..204,534] 🔴 -49.2% 272,432 ops/sec [256,439..274,813] → 129,982 ops/sec [129,029..132,275] 🔴 -52.3%
template literal 779,216 ops/sec [776,487..782,874] → 393,524 ops/sec [381,192..399,421] 🔴 -49.5% 487,098 ops/sec [475,091..491,586] → 231,483 ops/sec [227,605..234,097] 🔴 -52.5%
string repeat 395,878 ops/sec [394,959..399,482] → 217,307 ops/sec [192,028..219,982] 🔴 -45.1% 439,776 ops/sec [433,864..440,161] → 232,778 ops/sec [229,756..233,979] 🔴 -47.1%
split and join 136,412 ops/sec [136,113..136,750] → 74,749 ops/sec [72,793..76,896] 🔴 -45.2% 147,258 ops/sec [143,956..148,146] → 74,057 ops/sec [73,245..74,292] 🔴 -49.7%
indexOf and includes 170,705 ops/sec [169,907..171,299] → 88,189 ops/sec [85,859..90,497] 🔴 -48.3% 171,561 ops/sec [167,946..171,880] → 86,963 ops/sec [85,619..87,790] 🔴 -49.3%
toUpperCase and toLowerCase 250,975 ops/sec [250,363..252,742] → 135,355 ops/sec [134,223..136,621] 🔴 -46.1% 322,467 ops/sec [321,238..323,726] → 157,971 ops/sec [152,324..162,608] 🔴 -51.0%
slice and substring 152,813 ops/sec [151,644..153,300] → 82,982 ops/sec [80,607..84,088] 🔴 -45.7% 183,782 ops/sec [181,623..188,287] → 95,630 ops/sec [94,803..96,040] 🔴 -48.0%
trim operations 184,777 ops/sec [183,738..185,181] → 102,482 ops/sec [101,390..103,170] 🔴 -44.5% 236,359 ops/sec [235,425..237,011] → 117,187 ops/sec [115,717..119,272] 🔴 -50.4%
replace and replaceAll 202,259 ops/sec [201,832..202,593] → 111,724 ops/sec [110,030..115,113] 🔴 -44.8% 204,658 ops/sec [202,202..206,547] → 112,114 ops/sec [111,050..113,418] 🔴 -45.2%
startsWith and endsWith 134,081 ops/sec [133,477..134,596] → 74,401 ops/sec [73,120..75,239] 🔴 -44.5% 140,328 ops/sec [139,485..141,204] → 75,659 ops/sec [74,683..76,824] 🔴 -46.1%
padStart and padEnd 196,193 ops/sec [196,088..196,566] → 108,515 ops/sec [107,831..110,995] 🔴 -44.7% 213,311 ops/sec [212,263..214,184] → 120,018 ops/sec [116,636..186,118] 🔴 -43.7%
identity tag, no substitutions 485,258 ops/sec [483,318..486,491] → 243,372 ops/sec [242,028..244,506] 🔴 -49.8% 1,372,952 ops/sec [1,370,178..1,380,057] → 1,126,358 ops/sec [1,117,923..1,133,101] 🔴 -18.0%
tag with 1 substitution 88,472 ops/sec [88,228..89,091] → 49,062 ops/sec [48,274..49,584] 🔴 -44.5% 108,089 ops/sec [105,406..109,701] → 93,897 ops/sec [93,578..94,917] 🔴 -13.1%
tag with 3 substitutions 48,969 ops/sec [48,493..49,176] → 26,220 ops/sec [26,117..26,320] 🔴 -46.5% 62,694 ops/sec [62,456..64,149] → 53,068 ops/sec [52,598..53,865] 🔴 -15.4%
tag with 6 substitutions 29,578 ops/sec [29,405..29,707] → 15,887 ops/sec [15,597..16,046] 🔴 -46.3% 37,076 ops/sec [36,893..37,719] → 31,934 ops/sec [31,700..32,014] 🔴 -13.9%
String.raw, no substitutions 625,064 ops/sec [620,462..629,652] → 300,062 ops/sec [298,699..301,140] 🔴 -52.0% 562,636 ops/sec [557,531..565,743] → 469,541 ops/sec [466,235..471,782] 🔴 -16.5%
String.raw, 2 substitutions 436,461 ops/sec [432,104..441,438] → 207,496 ops/sec [201,355..218,013] 🔴 -52.5% 389,221 ops/sec [388,365..392,704] → 362,459 ops/sec [361,271..363,100] 🔴 -6.9%
tag accessing .raw array 175,378 ops/sec [175,133..176,620] → 153,870 ops/sec [93,133..155,140] 🔴 -12.3% 198,103 ops/sec [193,494..206,795] → 177,055 ops/sec [171,200..179,344] 🔴 -10.6%
method as tag (this binding) 64,673 ops/sec [64,611..64,984] → 58,567 ops/sec [57,607..59,416] 🔴 -9.4% 85,799 ops/sec [84,705..88,885] → 71,464 ops/sec [68,339..72,312] 🔴 -16.7%
typed-arrays.js — Interp: 🟢 3, 🔴 17, 2 unch. · avg -5.9% · Bytecode: 🟢 2, 🔴 19, 1 unch. · avg -13.4%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 337,520 ops/sec [336,845..338,749] → 292,626 ops/sec [290,941..294,498] 🔴 -13.3% 374,463 ops/sec [373,671..375,802] → 311,226 ops/sec [309,932..313,280] 🔴 -16.9%
new Int32Array(100) 304,185 ops/sec [303,073..304,615] → 263,375 ops/sec [262,713..264,995] 🔴 -13.4% 339,348 ops/sec [332,175..346,022] → 288,159 ops/sec [287,263..289,695] 🔴 -15.1%
new Int32Array(1000) 186,578 ops/sec [183,952..188,432] → 183,971 ops/sec [182,985..185,395] ~ overlap (-1.4%) 202,496 ops/sec [198,479..203,895] → 173,968 ops/sec [172,401..175,986] 🔴 -14.1%
new Float64Array(100) 280,535 ops/sec [280,096..281,530] → 249,584 ops/sec [248,036..251,594] 🔴 -11.0% 323,900 ops/sec [314,976..329,140] → 261,762 ops/sec [259,641..263,491] 🔴 -19.2%
Int32Array.from([...]) 178,795 ops/sec [178,067..179,012] → 187,073 ops/sec [186,660..187,942] 🟢 +4.6% 187,888 ops/sec [186,824..189,196] → 176,597 ops/sec [176,066..177,357] 🔴 -6.0%
Int32Array.of(1, 2, 3, 4, 5) 303,470 ops/sec [302,448..303,934] → 267,674 ops/sec [266,624..268,032] 🔴 -11.8% 357,471 ops/sec [351,020..359,554] → 292,391 ops/sec [290,186..294,371] 🔴 -18.2%
sequential write 100 elements 3,416 ops/sec [3,407..3,456] → 3,335 ops/sec [3,331..3,344] 🔴 -2.4% 11,109 ops/sec [11,102..11,110] → 11,446 ops/sec [11,429..11,608] 🟢 +3.0%
sequential read 100 elements 3,472 ops/sec [3,464..3,477] → 3,324 ops/sec [3,277..3,335] 🔴 -4.3% 14,995 ops/sec [14,840..15,075] → 11,744 ops/sec [11,408..11,800] 🔴 -21.7%
Float64Array write 100 elements 3,220 ops/sec [3,198..3,234] → 3,033 ops/sec [3,014..3,051] 🔴 -5.8% 7,920 ops/sec [7,669..8,002] → 7,851 ops/sec [7,841..7,901] ~ overlap (-0.9%)
fill(42) 45,647 ops/sec [45,536..45,681] → 60,387 ops/sec [60,301..60,448] 🟢 +32.3% 46,024 ops/sec [45,951..46,088] → 47,572 ops/sec [47,480..47,581] 🟢 +3.4%
slice() 199,041 ops/sec [198,503..199,425] → 200,592 ops/sec [199,392..200,925] ~ overlap (+0.8%) 228,076 ops/sec [226,538..228,517] → 205,118 ops/sec [204,095..206,338] 🔴 -10.1%
map(x => x * 2) 7,933 ops/sec [7,884..7,966] → 6,683 ops/sec [6,632..6,714] 🔴 -15.8% 9,913 ops/sec [9,882..9,985] → 8,073 ops/sec [7,970..8,185] 🔴 -18.6%
filter(x => x > 50) 8,260 ops/sec [8,043..8,315] → 6,968 ops/sec [6,894..6,995] 🔴 -15.6% 10,680 ops/sec [10,533..10,725] → 8,804 ops/sec [8,705..8,826] 🔴 -17.6%
reduce (sum) 8,153 ops/sec [8,003..8,191] → 6,696 ops/sec [6,664..6,758] 🔴 -17.9% 9,956 ops/sec [9,817..10,080] → 7,735 ops/sec [7,721..7,831] 🔴 -22.3%
sort() 168,492 ops/sec [167,216..168,637] → 186,620 ops/sec [185,852..187,073] 🟢 +10.8% 175,727 ops/sec [175,289..175,858] → 170,302 ops/sec [169,958..170,391] 🔴 -3.1%
indexOf() 434,634 ops/sec [429,654..436,748] → 386,850 ops/sec [385,398..387,792] 🔴 -11.0% 501,895 ops/sec [500,176..504,039] → 448,051 ops/sec [447,174..449,251] 🔴 -10.7%
reverse() 327,896 ops/sec [327,502..328,674] → 323,882 ops/sec [322,693..324,727] 🔴 -1.2% 360,524 ops/sec [360,328..360,961] → 339,351 ops/sec [338,519..340,399] 🔴 -5.9%
create view over existing buffer 412,667 ops/sec [411,830..413,990] → 348,108 ops/sec [346,937..349,535] 🔴 -15.6% 466,019 ops/sec [463,192..466,614] → 388,368 ops/sec [386,008..390,551] 🔴 -16.7%
subarray() 419,990 ops/sec [417,399..422,971] → 373,111 ops/sec [369,686..376,120] 🔴 -11.2% 529,200 ops/sec [526,709..535,896] → 442,636 ops/sec [437,994..446,336] 🔴 -16.4%
set() from array 571,639 ops/sec [565,856..573,206] → 490,699 ops/sec [489,381..492,132] 🔴 -14.2% 637,832 ops/sec [632,658..639,329] → 556,660 ops/sec [554,936..558,694] 🔴 -12.7%
for-of loop 4,926 ops/sec [4,900..4,942] → 4,630 ops/sec [4,599..4,637] 🔴 -6.0% 26,756 ops/sec [26,540..26,905] → 19,306 ops/sec [18,995..19,544] 🔴 -27.8%
spread into array 17,117 ops/sec [17,044..17,178] → 15,977 ops/sec [15,888..16,068] 🔴 -6.7% 106,093 ops/sec [103,478..107,197] → 77,037 ops/sec [76,187..78,024] 🔴 -27.4%
uint8array-encoding.js — Interp: 🔴 18 · avg -26.1% · Bytecode: 🔴 17, 1 unch. · avg -28.3%
Benchmark Interpreted Δ Bytecode Δ
short (5 bytes) 780,488 ops/sec [778,155..782,425] → 341,920 ops/sec [340,689..343,916] 🔴 -56.2% 1,189,967 ops/sec [1,181,086..1,200,124] → 927,965 ops/sec [919,368..940,499] 🔴 -22.0%
medium (450 bytes) 284,648 ops/sec [282,061..284,760] → 177,900 ops/sec [177,222..178,128] 🔴 -37.5% 348,408 ops/sec [347,699..348,508] → 294,270 ops/sec [292,063..295,525] 🔴 -15.5%
large (4096 bytes) 44,890 ops/sec [43,939..45,256] → 34,623 ops/sec [32,624..34,959] 🔴 -22.9% 50,474 ops/sec [44,778..50,649] → 34,913 ops/sec [34,071..36,384] 🔴 -30.8%
base64url alphabet 199,523 ops/sec [196,164..200,939] → 121,785 ops/sec [120,279..122,707] 🔴 -39.0% 217,547 ops/sec [216,161..220,236] → 128,169 ops/sec [126,585..129,527] 🔴 -41.1%
omitPadding 366,426 ops/sec [364,771..367,754] → 195,966 ops/sec [195,204..196,872] 🔴 -46.5% 433,522 ops/sec [424,418..438,096] → 225,234 ops/sec [223,049..227,516] 🔴 -48.0%
short (8 chars) 336,539 ops/sec [334,257..340,069] → 168,313 ops/sec [166,943..169,527] 🔴 -50.0% 376,000 ops/sec [370,803..376,557] → 194,394 ops/sec [192,402..196,321] 🔴 -48.3%
medium (600 chars) 144,614 ops/sec [144,269..145,057] → 76,745 ops/sec [76,238..76,955] 🔴 -46.9% 150,597 ops/sec [150,068..151,947] → 80,474 ops/sec [80,063..80,654] 🔴 -46.6%
large (5464 chars) 25,738 ops/sec [25,078..25,823] → 14,342 ops/sec [13,835..14,488] 🔴 -44.3% 26,653 ops/sec [26,335..26,724] → 14,148 ops/sec [14,134..14,548] 🔴 -46.9%
short (5 bytes) 845,300 ops/sec [817,811..848,636] → 642,541 ops/sec [639,598..648,365] 🔴 -24.0% 1,207,013 ops/sec [1,196,425..1,212,021] → 568,567 ops/sec [566,256..582,238] 🔴 -52.9%
medium (450 bytes) 266,760 ops/sec [265,411..267,919] → 256,857 ops/sec [155,315..261,257] 🔴 -3.7% 312,034 ops/sec [310,488..313,462] → 169,320 ops/sec [158,743..169,789] 🔴 -45.7%
large (4096 bytes) 41,820 ops/sec [40,982..42,069] → 28,031 ops/sec [26,847..28,552] 🔴 -33.0% 41,821 ops/sec [40,923..47,439] → 28,830 ops/sec [23,661..41,113] ~ overlap (-31.1%)
short (10 chars) 373,310 ops/sec [369,777..374,222] → 324,371 ops/sec [321,216..326,223] 🔴 -13.1% 418,469 ops/sec [415,971..423,206] → 337,483 ops/sec [335,122..338,560] 🔴 -19.4%
medium (900 chars) 235,665 ops/sec [235,065..236,478] → 205,446 ops/sec [204,564..206,312] 🔴 -12.8% 245,561 ops/sec [241,175..246,672] → 218,528 ops/sec [216,463..219,188] 🔴 -11.0%
large (8192 chars) 58,026 ops/sec [57,690..58,123] → 55,334 ops/sec [55,137..55,427] 🔴 -4.6% 58,744 ops/sec [58,587..58,965] → 54,434 ops/sec [54,032..54,690] 🔴 -7.3%
setFromBase64 (450 bytes) 133,469 ops/sec [132,445..133,962] → 116,637 ops/sec [115,541..117,075] 🔴 -12.6% 144,970 ops/sec [143,212..145,731] → 132,683 ops/sec [131,548..133,889] 🔴 -8.5%
setFromHex (450 bytes) 203,378 ops/sec [202,320..203,939] → 184,251 ops/sec [183,880..184,278] 🔴 -9.4% 224,291 ops/sec [221,864..224,994] → 198,282 ops/sec [196,782..198,660] 🔴 -11.6%
toBase64 → fromBase64 (450 bytes) 100,385 ops/sec [99,948..100,844] → 93,372 ops/sec [92,945..94,664] 🔴 -7.0% 104,749 ops/sec [104,273..105,088] → 96,243 ops/sec [95,794..96,889] 🔴 -8.1%
toHex → fromHex (450 bytes) 131,046 ops/sec [130,843..131,304] → 123,305 ops/sec [123,137..123,897] 🔴 -5.9% 146,816 ops/sec [145,053..147,284] → 125,151 ops/sec [123,901..125,980] 🔴 -14.8%

Measured on ubuntu-latest x64. Benchmark ranges compare cached main-branch min/max ops/sec with the PR run; overlapping ranges are treated as unchanged noise. Percentage deltas are secondary context.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 5358 5358
Tests Passed 5317 ✅ 5358 ✅
Tests Skipped 41 0
Tests Test Duration 896.2ms 876.3ms
Tests Lex 234.6ms 147.3ms
Tests Parse 246.1ms 262.6ms
Tests Compile 158.5ms
Tests Execute 930.8ms 947.1ms
Tests Engine Total 1.41s 1.52s
Benchmarks Total 364 364
Benchmarks Duration 8.25min 7.80min

Measured on ubuntu-latest x64.

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: 10

Caution

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

⚠️ Outside diff range comments (2)
units/Goccia.SharedPrototype.pas (1)

51-62: ⚠️ Potential issue | 🟠 Major

Refresh prototype.constructor on every expose.

TGocciaSharedPrototype outlives any single engine on a thread. With this guard, the throwaway pre-init engine — or just the first engine on that thread — permanently owns *.prototype.constructor, so later engines observe a stale constructor and keep the old object graph alive.

Suggested fix
-  if not FPrototype.HasProperty(PROP_CONSTRUCTOR) then
-    FPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor);
+  FPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.SharedPrototype.pas` around lines 51 - 62,
TGocciaSharedPrototype.ExposeOnConstructor currently skips writing
PROP_CONSTRUCTOR if it's already present, which lets the first engine on a
thread permanently own prototype.constructor; change ExposeOnConstructor so it
always refreshes the constructor on FPrototype (i.e., remove the conditional
HasProperty check and unconditionally call
FPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor) after handling
TGocciaClassValue/TGocciaObjectValue) so every expose updates the shared
prototype.constructor to the current AConstructor.
units/Goccia.Values.ArrayValue.pas (1)

327-336: ⚠️ Potential issue | 🟠 Major

Keep Array.prototype.constructor synced with the current engine.

FSharedArrayPrototype lives for the whole thread. After EnsureSharedPrototypesInitialized — or any earlier engine on the same thread — sets constructor, later engines skip this write, so Array.prototype.constructor and ArraySpeciesCreate() keep using the stale constructor from the old engine.

Suggested fix
-  if not FSharedArrayPrototype.HasProperty(PROP_CONSTRUCTOR) then
-    FSharedArrayPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor);
+  FSharedArrayPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor);
🤖 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 327 - 336, The code
currently skips setting Array.prototype.constructor if FSharedArrayPrototype
already has PROP_CONSTRUCTOR, which leaves a stale constructor from a previous
engine; in TGocciaArrayValue.ExposePrototype always (re)assign PROP_CONSTRUCTOR
on FSharedArrayPrototype to the supplied AConstructor so the prototype is kept
in sync with the current engine — replace the conditional HasProperty check and
ensure you use the appropriate assign/replace method (e.g., AssignProperty or a
ReplaceProperty overload) to overwrite PROP_CONSTRUCTOR with AConstructor.
🧹 Nitpick comments (6)
units/Goccia.Builtins.Math.pas (1)

81-82: Make the member-definition array local.

FStaticMembers is assigned on Line 146 and consumed on Line 150 inside the same constructor. It never acts as a cache, so the new threadvar just adds extra mutable state to reason about. A local Definitions variable would keep the thread-safety benefit without storing a managed array in thread-local storage.

Also applies to: 86-150

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

In `@units/Goccia.Builtins.Math.pas` around lines 81 - 82, Remove the threadvar
FStaticMembers and instead declare a local TArray<TGocciaMemberDefinition>
variable (e.g., Definitions) inside the constructor where FStaticMembers is
assigned and consumed (the code around the current assignments at the
TGocciaBuiltinsMath constructor). Replace uses of FStaticMembers in that
constructor with the local Definitions variable, ensure you populate Definitions
the same way as before, and verify there are no other references to
FStaticMembers elsewhere (if there are, convert them to use a local or refactor
accordingly); this keeps thread-safety without storing a managed array in
thread-local storage.
units/Goccia.Builtins.JSONL.pas (1)

52-54: Use a local definitions array here.

FStaticMembers is written on Line 71 and read once on Line 76, with no later reads in the unit. The threadvar doesn't buy reuse here and can be a constructor-local variable instead.

Also applies to: 55-76

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

In `@units/Goccia.Builtins.JSONL.pas` around lines 52 - 54, The threadvar
FStaticMembers is unnecessary because it's only written once and read once
within the same initialization scope; remove the threadvar declaration and
instead declare a local variable (e.g., LocalMembers:
TArray<TGocciaMemberDefinition>) in the routine that currently assigns/uses
FStaticMembers (the constructor/initializer where FStaticMembers is set at the
assignment site and read later). Replace references to FStaticMembers with the
local variable (use the local while building the member definitions) and delete
the threadvar/FStaticMembers declaration so the array lifetime is limited to the
routine.
units/Goccia.Values.TemporalPlainMonthDay.pas (1)

56-58: FPrototypeMembers is dead cache state here.

Once Line 161 starts returning early, this array is built once and never reread. Keeping FShared thread-local but making the member-definition array local would simplify the prototype-init path.

Also applies to: 157-181

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

In `@units/Goccia.Values.TemporalPlainMonthDay.pas` around lines 56 - 58,
FPrototypeMembers is a stale threadvar cache causing the prototype init to only
build once; change FPrototypeMembers from a threadvar field into a local
variable used inside the prototype-initialization routine (where
TGocciaSharedPrototype/FShared is referenced) so the member-definition array is
constructed on each init-path instead of being kept thread-local. Locate usages
of FPrototypeMembers and the prototype setup function that currently reads/sets
it (references: FPrototypeMembers, FShared, TGocciaSharedPrototype,
TGocciaMemberDefinition) and move the array declaration into that routine,
update assignments/usages to the local variable, and remove the threadvar
declaration to simplify and ensure fresh member-definition construction (apply
same change pattern to the other similar block noted around the prototype init).
units/Goccia.Values.NumberObjectValue.pas (1)

51-55: Localize FPrototypeMembers; keep the reusable thread-local pieces.

FSharedNumberPrototype and FPrototypeMethodHost are the actual per-thread state here. FPrototypeMembers isn't: after Line 94 starts exiting early, the array built on Lines 98-107 is never reread.

Also applies to: 90-118

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

In `@units/Goccia.Values.NumberObjectValue.pas` around lines 51 - 55,
FPrototypeMembers should not be a threadvar because it’s rebuilt each time and
left stale when early exits occur; keep FSharedNumberPrototype and
FPrototypeMethodHost as thread-local but remove FPrototypeMembers from the
threadvar block and instead declare it as a local variable inside the routine
that constructs the number prototype (the code that currently populates
FPrototypeMembers in the TGocciaNumberObjectValue/TGocciaObjectValue prototype
init). Update the constructor/initializer to use the new local
TArray<TGocciaMemberDefinition> variable when building members (and free/leave
scope on exit), and remove references to the global FPrototypeMembers; apply the
same change to the adjacent prototype-building code paths noted (the 90-118
region) so no shared persistent array remains.
units/Goccia.Builtins.GlobalObject.pas (1)

68-70: FStaticMembers is effectively a temporary.

The constructor writes it on Line 164 and immediately consumes it on Line 168. A local variable would remove unnecessary thread-local state and keep this change focused on data that's actually shared across calls.

Also applies to: 133-168

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

In `@units/Goccia.Builtins.GlobalObject.pas` around lines 68 - 70, FStaticMembers
is declared as a threadvar but is only written in the constructor and
immediately consumed, so remove the thread-local field and use a local variable
instead: delete the threadvar FStaticMembers declaration and in the constructor
(where FStaticMembers is currently assigned) introduce a local variable (e.g.,
LocalMembers: TArray<TGocciaMemberDefinition>) to build the
TGocciaMemberDefinition array, then pass/consume that local array in the same
places the constructor currently uses FStaticMembers; also update any code in
the 133–168 region that references FStaticMembers to use the local variable or
the consumer call so no thread-local state remains.
units/Goccia.Benchmark.Reporter.pas (1)

108-111: Fail fast on out-of-range Files[] access.

GetFileResult is now part of the public surface, but it does raw dynamic-array indexing. With range checks off, an invalid index becomes undefined memory access instead of a clear failure. Add an explicit bounds check here so misuse fails deterministically.

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

In `@units/Goccia.Benchmark.Reporter.pas` around lines 108 - 111,
TBenchmarkReporter.GetFileResult currently returns FFiles[AIndex] without
validation; add an explicit bounds check against Length(FFiles) (or Low/High)
and raise a clear exception (e.g., ERangeError or a custom exception) when
AIndex is out of range so callers fail deterministically instead of producing
undefined behavior; update TBenchmarkReporter.GetFileResult to validate AIndex
before accessing FFiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ScriptLoader.dpr`:
- Around line 692-707: The worker should not call RunScriptFromFile directly
because that lets the script runtime write to stdout from background threads;
instead modify TScriptLoaderApp.ScriptWorkerProc to invoke RunSource (not
RunScriptFromFile), capture its returned console text and any error text into
AConsoleOutput and AErrorMessage, and propagate failures by setting ExitCode = 1
when RunSource indicates an error or raises; locate ScriptWorkerProc and replace
the direct RunScriptFromFile call with a call to RunSource (or the function that
returns printed output/errors), assign its outputs to
AConsoleOutput/AErrorMessage, and ensure exceptions are caught and marshaled
into AErrorMessage so RunScriptsParallel on the main thread can report failures
in order.

In `@TestRunner.dpr`:
- Around line 227-228: The parallel branch is chosen whenever
GetJobCount(Files.Count) > 1 which bypasses the CLI --exit-on-first-failure
behavior; update the condition so you only spawn RunScriptsFromFilesParallel
when GetJobCount(Files.Count) > 1 AND the exit-on-first-failure flag is not set
(e.g. check the boolean backing --exit-on-first-failure), otherwise call the
sequential runner that respects FailedCount and exits on first failure; change
the existing else-if around RunScriptsFromFilesParallel to include this
additional flag check (referencing GetJobCount, RunScriptsFromFilesParallel,
PrintTestResults and the exit-on-first-failure flag and FailedCount).

In `@units/Goccia.Builtins.GlobalPromise.pas`:
- Around line 56-57: The threadvar FStaticMembers (and the same pattern in the
other builtins) holds a managed dynamic array and leaks because Free Pascal
doesn't finalize managed threadvar data on thread exit; fix by removing managed
types from thread-local storage and adding explicit cleanup: either (preferred)
replace the threadvar FStaticMembers with a thread-safe singleton/cache or
instance-scoped field (e.g., a TGocciaBuiltinCache that exposes GetStaticMembers
per-thread) so managed arrays are owned and finalized by a regular object, or
(quick patch) register a thread-exit cleanup hook that sets FStaticMembers :=
nil for each affected unit (GlobalPromise/FStaticMembers and the other listed
builtin units) before the thread exits; apply the same consistent change to all
17 affected units.

In `@units/Goccia.Builtins.GlobalRegExp.pas`:
- Around line 68-70: GRegExpPrototype is a shared global causing races between
parallel workers; change its declaration from a plain var to a thread-local
variable so each thread has its own prototype. Update the declaration of
GRegExpPrototype in units/Goccia.RegExp.Runtime.pas to use threadvar instead of
var, leaving SetRegExpPrototype and CreateRegExpObject behavior intact so each
thread sets/reads its own GRegExpPrototype and avoids cross-thread prototype
overwrites. Ensure any references to GRegExpPrototype elsewhere compile
unchanged.

In `@units/Goccia.Coverage.pas`:
- Around line 406-422: Worker threads never initialize or merge coverage because
TGocciaCoverageTracker is a threadvar and workers don't call Initialize; update
TGocciaFileWorker.Execute (after InitThreadRuntime) to call
TGocciaCoverageTracker.Initialize and enable the tracker for that thread, and
ensure Shutdown is called on worker exit; add a per-worker export method (e.g.,
TGocciaCoverageTracker.ExportCoverage or similar) that serializes the worker's
file/line hit data and registered source-file metadata before shutdown, send or
store that export for the main thread, and implement a corresponding
merge/import routine on the main thread to merge worker exports into the main
tracker (avoiding GetOrCreateFile(..., 0) fallback by preserving original file
metadata such as executable-line counts and IDs during export/import); ensure
RegisterSourceFile and all Record* paths use the per-thread tracker when present
and that merged files reconcile counts and hits into the main
TGocciaCoverageTracker.Instance before final reporting.

In `@units/Goccia.ObjectModel.Engine.pas`:
- Around line 113-115: The current guard prevents rebinding
prototype.constructor when prototypes are shared across worker runs: remove the
conditional check and always set PROP_CONSTRUCTOR after ReplacePrototype so each
engine run resets the constructor reference; specifically, change the logic
around AConstructor.ReplacePrototype(ATypeDefinition.PrototypeProvider()) /
AConstructor.Prototype.HasProperty(PROP_CONSTRUCTOR) so that
AConstructor.Prototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor) is invoked
unconditionally (or use an explicit replace method) to ensure
prototype.constructor points to the current AConstructor every run.

In `@units/Goccia.Threading.pas`:
- Around line 252-259: When a worker thread has a fatal exception we must not
copy its uninitialized default Result slots into the shared FResults (they have
Index = 0), so change the loop around Workers[I].Results: either skip the whole
copy when Assigned(Workers[I].FatalException) is true, or if you still want to
preserve any partial results copy only entries that are initialized (e.g. check
Results[ResultIdx].Index <> 0 or another sentinel) before assigning into
FResults; update the code paths around Workers, FatalException, Results and
FResults so only valid per-file results are propagated.

In `@units/Goccia.Timeout.pas`:
- Around line 26-29: The thread-local timeout variables (GTimeoutMilliseconds,
GStartNanoseconds, GCheckCounter) are never initialized on worker threads
because StartExecutionTimeout is only called on the main thread; update each
worker to initialize per-thread timeout state by invoking StartExecutionTimeout
at the start of TestWorkerProc, ScriptWorkerProc, and BenchmarkWorkerProc (or
alternatively replace the threadvar design with a shared, synchronized timeout
structure and adjust CheckExecutionTimeout accordingly) so that
CheckExecutionTimeout in worker threads reads a properly initialized timeout
state.

In `@units/Goccia.Values.FunctionBase.pas`:
- Around line 257-259: The threadvar FSharedPrototype is assigned to Self too
early and must be cleared if construction/registration fails; modify
TGocciaFunctionBase.Create (and any code paths that set FSharedPrototype) to set
FSharedPrototype := Self only temporarily and ensure a try..except/try..finally
clears FSharedPrototype when an exception occurs (or when construction did not
complete successfully), so that a half-constructed instance isn't left for later
calls to TGocciaFunctionBase.Create or SetSharedPrototypeParent to reuse; in
short, wrap the post-assignment registration steps and on error reset
FSharedPrototype to nil.

In `@units/Goccia.Values.SymbolValue.pas`:
- Around line 85-88: The prototype cache was made thread-local
(FSharedPrototype/FMethodHost) but the symbol system remains process-global, so
concurrent TGocciaSymbolValue.Create calls can race on
GNextSymbolId/GSymbolRegistry and well-known symbol singletons; fix by making
the symbol registry and well-known symbol singletons thread-local (threadvar) or
serialize access with a global critical section around all accesses/updates to
GNextSymbolId and GSymbolRegistry and when initializing well-known
TGocciaSymbolValue instances, update TGocciaSymbolValue.Create and any registry
lookup/registration paths to use the chosen protection (thread-local registry or
lock) and ensure proper per-thread cleanup/initialization for the well-known
symbols to avoid dangling pointers after a worker GC shutdown.

---

Outside diff comments:
In `@units/Goccia.SharedPrototype.pas`:
- Around line 51-62: TGocciaSharedPrototype.ExposeOnConstructor currently skips
writing PROP_CONSTRUCTOR if it's already present, which lets the first engine on
a thread permanently own prototype.constructor; change ExposeOnConstructor so it
always refreshes the constructor on FPrototype (i.e., remove the conditional
HasProperty check and unconditionally call
FPrototype.AssignProperty(PROP_CONSTRUCTOR, AConstructor) after handling
TGocciaClassValue/TGocciaObjectValue) so every expose updates the shared
prototype.constructor to the current AConstructor.

In `@units/Goccia.Values.ArrayValue.pas`:
- Around line 327-336: The code currently skips setting
Array.prototype.constructor if FSharedArrayPrototype already has
PROP_CONSTRUCTOR, which leaves a stale constructor from a previous engine; in
TGocciaArrayValue.ExposePrototype always (re)assign PROP_CONSTRUCTOR on
FSharedArrayPrototype to the supplied AConstructor so the prototype is kept in
sync with the current engine — replace the conditional HasProperty check and
ensure you use the appropriate assign/replace method (e.g., AssignProperty or a
ReplaceProperty overload) to overwrite PROP_CONSTRUCTOR with AConstructor.

---

Nitpick comments:
In `@units/Goccia.Benchmark.Reporter.pas`:
- Around line 108-111: TBenchmarkReporter.GetFileResult currently returns
FFiles[AIndex] without validation; add an explicit bounds check against
Length(FFiles) (or Low/High) and raise a clear exception (e.g., ERangeError or a
custom exception) when AIndex is out of range so callers fail deterministically
instead of producing undefined behavior; update TBenchmarkReporter.GetFileResult
to validate AIndex before accessing FFiles.

In `@units/Goccia.Builtins.GlobalObject.pas`:
- Around line 68-70: FStaticMembers is declared as a threadvar but is only
written in the constructor and immediately consumed, so remove the thread-local
field and use a local variable instead: delete the threadvar FStaticMembers
declaration and in the constructor (where FStaticMembers is currently assigned)
introduce a local variable (e.g., LocalMembers: TArray<TGocciaMemberDefinition>)
to build the TGocciaMemberDefinition array, then pass/consume that local array
in the same places the constructor currently uses FStaticMembers; also update
any code in the 133–168 region that references FStaticMembers to use the local
variable or the consumer call so no thread-local state remains.

In `@units/Goccia.Builtins.JSONL.pas`:
- Around line 52-54: The threadvar FStaticMembers is unnecessary because it's
only written once and read once within the same initialization scope; remove the
threadvar declaration and instead declare a local variable (e.g., LocalMembers:
TArray<TGocciaMemberDefinition>) in the routine that currently assigns/uses
FStaticMembers (the constructor/initializer where FStaticMembers is set at the
assignment site and read later). Replace references to FStaticMembers with the
local variable (use the local while building the member definitions) and delete
the threadvar/FStaticMembers declaration so the array lifetime is limited to the
routine.

In `@units/Goccia.Builtins.Math.pas`:
- Around line 81-82: Remove the threadvar FStaticMembers and instead declare a
local TArray<TGocciaMemberDefinition> variable (e.g., Definitions) inside the
constructor where FStaticMembers is assigned and consumed (the code around the
current assignments at the TGocciaBuiltinsMath constructor). Replace uses of
FStaticMembers in that constructor with the local Definitions variable, ensure
you populate Definitions the same way as before, and verify there are no other
references to FStaticMembers elsewhere (if there are, convert them to use a
local or refactor accordingly); this keeps thread-safety without storing a
managed array in thread-local storage.

In `@units/Goccia.Values.NumberObjectValue.pas`:
- Around line 51-55: FPrototypeMembers should not be a threadvar because it’s
rebuilt each time and left stale when early exits occur; keep
FSharedNumberPrototype and FPrototypeMethodHost as thread-local but remove
FPrototypeMembers from the threadvar block and instead declare it as a local
variable inside the routine that constructs the number prototype (the code that
currently populates FPrototypeMembers in the
TGocciaNumberObjectValue/TGocciaObjectValue prototype init). Update the
constructor/initializer to use the new local TArray<TGocciaMemberDefinition>
variable when building members (and free/leave scope on exit), and remove
references to the global FPrototypeMembers; apply the same change to the
adjacent prototype-building code paths noted (the 90-118 region) so no shared
persistent array remains.

In `@units/Goccia.Values.TemporalPlainMonthDay.pas`:
- Around line 56-58: FPrototypeMembers is a stale threadvar cache causing the
prototype init to only build once; change FPrototypeMembers from a threadvar
field into a local variable used inside the prototype-initialization routine
(where TGocciaSharedPrototype/FShared is referenced) so the member-definition
array is constructed on each init-path instead of being kept thread-local.
Locate usages of FPrototypeMembers and the prototype setup function that
currently reads/sets it (references: FPrototypeMembers, FShared,
TGocciaSharedPrototype, TGocciaMemberDefinition) and move the array declaration
into that routine, update assignments/usages to the local variable, and remove
the threadvar declaration to simplify and ensure fresh member-definition
construction (apply same change pattern to the other similar block noted around
the prototype init).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a76e37ae-0269-4d8b-84da-dd3eeb1c2bef

📥 Commits

Reviewing files that changed from the base of the PR and between fe8c8fa and c00e6a4.

📒 Files selected for processing (65)
  • BenchmarkRunner.dpr
  • REPL.dpr
  • ScriptLoader.dpr
  • TestRunner.dpr
  • units/Goccia.Benchmark.Reporter.pas
  • units/Goccia.Builtins.Console.pas
  • units/Goccia.Builtins.GlobalArray.pas
  • units/Goccia.Builtins.GlobalArrayBuffer.pas
  • units/Goccia.Builtins.GlobalFFI.pas
  • units/Goccia.Builtins.GlobalNumber.pas
  • units/Goccia.Builtins.GlobalObject.pas
  • units/Goccia.Builtins.GlobalPromise.pas
  • units/Goccia.Builtins.GlobalReflect.pas
  • units/Goccia.Builtins.GlobalRegExp.pas
  • units/Goccia.Builtins.GlobalString.pas
  • units/Goccia.Builtins.GlobalSymbol.pas
  • units/Goccia.Builtins.GlobalURL.pas
  • units/Goccia.Builtins.Globals.pas
  • units/Goccia.Builtins.JSON.pas
  • units/Goccia.Builtins.JSON5.pas
  • units/Goccia.Builtins.JSONL.pas
  • units/Goccia.Builtins.Math.pas
  • units/Goccia.Builtins.Performance.pas
  • units/Goccia.Builtins.TOML.pas
  • units/Goccia.Builtins.YAML.pas
  • units/Goccia.CLI.Application.pas
  • units/Goccia.CallStack.pas
  • units/Goccia.Coverage.pas
  • units/Goccia.GarbageCollector.pas
  • units/Goccia.MicrotaskQueue.pas
  • units/Goccia.ObjectModel.Engine.pas
  • units/Goccia.ObjectModel.pas
  • units/Goccia.Profiler.pas
  • units/Goccia.SharedPrototype.pas
  • units/Goccia.Threading.Init.pas
  • units/Goccia.Threading.pas
  • units/Goccia.Timeout.pas
  • units/Goccia.Values.ArrayBufferValue.pas
  • units/Goccia.Values.ArrayValue.pas
  • units/Goccia.Values.BooleanObjectValue.pas
  • units/Goccia.Values.FFILibrary.pas
  • units/Goccia.Values.FFIPointer.pas
  • units/Goccia.Values.FunctionBase.pas
  • units/Goccia.Values.IteratorValue.pas
  • units/Goccia.Values.MapValue.pas
  • units/Goccia.Values.NumberObjectValue.pas
  • units/Goccia.Values.ObjectValue.pas
  • units/Goccia.Values.PromiseValue.pas
  • units/Goccia.Values.SetValue.pas
  • units/Goccia.Values.SharedArrayBufferValue.pas
  • units/Goccia.Values.StringObjectValue.pas
  • units/Goccia.Values.SymbolValue.pas
  • units/Goccia.Values.TemporalDuration.pas
  • units/Goccia.Values.TemporalInstant.pas
  • units/Goccia.Values.TemporalPlainDate.pas
  • units/Goccia.Values.TemporalPlainDateTime.pas
  • units/Goccia.Values.TemporalPlainMonthDay.pas
  • units/Goccia.Values.TemporalPlainTime.pas
  • units/Goccia.Values.TemporalPlainYearMonth.pas
  • units/Goccia.Values.TemporalZonedDateTime.pas
  • units/Goccia.Values.TextDecoderValue.pas
  • units/Goccia.Values.TextEncoderValue.pas
  • units/Goccia.Values.TypedArrayValue.pas
  • units/Goccia.Values.URLSearchParamsValue.pas
  • units/Goccia.Values.URLValue.pas

Comment thread ScriptLoader.dpr
Comment on lines +692 to +707
procedure TScriptLoaderApp.ScriptWorkerProc(const AFileName: string;
const AIndex: Integer; out AConsoleOutput: string;
out AErrorMessage: string; AData: Pointer);
begin
AConsoleOutput := '';
AErrorMessage := '';
try
RunScriptFromFile(AFileName);
except
on E: Exception do
begin
AErrorMessage := E.Message;
ExitCode := 1;
end;
end;
end;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 14, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't run RunScriptFromFile directly inside the worker.

RunSource prints results/warnings/errors itself and catches most script exceptions, so this worker still writes from background threads while leaving AConsoleOutput / AErrorMessage mostly empty. In parallel runs that makes stdout ordering nondeterministic and prevents RunScriptsParallel from reporting failures in file order on the main thread.

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

In `@ScriptLoader.dpr` around lines 692 - 707, The worker should not call
RunScriptFromFile directly because that lets the script runtime write to stdout
from background threads; instead modify TScriptLoaderApp.ScriptWorkerProc to
invoke RunSource (not RunScriptFromFile), capture its returned console text and
any error text into AConsoleOutput and AErrorMessage, and propagate failures by
setting ExitCode = 1 when RunSource indicates an error or raises; locate
ScriptWorkerProc and replace the direct RunScriptFromFile call with a call to
RunSource (or the function that returns printed output/errors), assign its
outputs to AConsoleOutput/AErrorMessage, and ensure exceptions are caught and
marshaled into AErrorMessage so RunScriptsParallel on the main thread can report
failures in order.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid concern — RunScriptFromFile does call WriteLn internally. For this initial implementation, interleaved stdout on parallel script execution is an accepted trade-off since ScriptLoader parallel mode is only used for directory inputs where output ordering is less critical. A proper fix would require refactoring the entire RunSource/ExecuteInterpreted chain to support output capture, which is a larger change. Filed as a follow-up.

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.

@frostney, understood — thanks for the context. The trade-off makes sense given that directory-mode parallel execution is the only trigger and strict output ordering is less critical there. I'll keep this in mind for future reviews.


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: ScriptLoader.dpr:692-707
Timestamp: 2026-04-15T06:26:10.118Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `TScriptLoaderApp.ScriptWorkerProc` intentionally calls `RunScriptFromFile` directly in worker threads, which causes interleaved/nondeterministic stdout in parallel directory runs. This is an accepted trade-off for the initial parallel implementation — ScriptLoader parallel mode is only used for directory inputs where output ordering is less critical. A proper fix (refactoring `RunSource`/`ExecuteInterpreted` to support output capture and returning text via `AConsoleOutput`/`AErrorMessage`) has been deferred to a follow-up. Do not re-flag the direct `RunScriptFromFile` call in `ScriptWorkerProc` as a regression or issue until the output-capture refactor lands.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: ScriptLoader.dpr:724-737
Timestamp: 2026-04-14T06:46:14.795Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `--emit` with `--output=<file>` and multiple input paths or a directory input silently overwrites the same output file for each compiled module. This is pre-existing behavior preserved across the CLI refactor in PR `#299` and should NOT be re-flagged as a regression. A follow-up guard (raising TGocciaParseError when FEmit.Present and FOutputPath.Present and APaths.Count > 1 or the single path is a directory) was proposed but deferred.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: TestRunner.dpr:237-240
Timestamp: 2026-04-07T20:24:01.235Z
Learning: Repo: frostney/GocciaScript — TestRunner.dpr / Goccia.Coverage
The synthetic `runTests({...})` call is appended to `Source` before lexing in both `RunGocciaScriptBytecode` and `RunGocciaScriptInterpreted`. `Lexer.SourceLines` therefore includes this line, and `CountExecutableLines` counts it as +1 executable. Because the engine always executes it, coverage also records +1 hit. The net effect on the percentage is self-consistent (+1/+1) and does NOT skew coverage ratios. `PrintCoverageDetail` reads the original file from disk, so the synthetic line never appears in the detail output. Do not flag this as a coverage accuracy bug.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 289
File: ScriptLoader.dpr:140-156
Timestamp: 2026-04-12T21:19:18.811Z
Learning: For GocciaScript CLI tools, suppress human-readable status/progress WriteLn output when running in JSON output mode. In ScriptLoader (ScriptLoader.dpr), gate any status/progress WriteLn lines on `not GJsonOutput`, because `--output=json` streams the result document directly to stdout (mixing status text would corrupt JSON). In TestRunner and BenchmarkRunner (TestRunner.dpr, BenchmarkRunner.dpr), JSON is written to a file, so current unconditional stdout WriteLn calls are safe, but these tools should be refactored later to follow the same stdout/JSON suppression pattern used by ScriptLoader.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T06:46:10.919Z
Learning: In frostney/GocciaScript CLI tool sources, the duplicated `EscapeJSONString` helper intentionally escapes only `\`, `"`, `\n`, `\r`, and `\t` (leaving other C0 control characters U+0000–U+001F unescaped). During code review, do not flag missing additional control-character escaping in these per-tool `EscapeJSONString` copies while the shared-helper consolidation/fix PR is not yet landed; only re-check once the implementation has been consolidated into the shared helper and the tracking fix is in place.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T13:53:18.321Z
Learning: For GocciaScript CLI tool sources in this repo, when you see duplicated `EscapeJSONString` helpers that only escape `\`, `"`, `\n`, `\r`, and `\t` (leaving other C0 control characters U+0000–U+001F unescaped) in individual per-tool `.dpr` files, do not re-flag this as a review issue while the consolidation/fix tracked in issue `#303` has not landed. After the PR resolving `#303` is merged, start reviewing these helpers for full control-character JSON escaping again.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 289
File: ScriptLoader.dpr:649-650
Timestamp: 2026-04-12T21:08:07.595Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr, units/Goccia.Engine.pas): `--source-map` is valid in interpreted mode when JSX preprocessing is active. Source maps are produced by the JSX pre-pass inside `ParseSource`/`TGocciaEngine`, not solely by the bytecode pipeline. `ExecuteInterpreted` needs to surface the engine's source map (via a public `SourceMap` property on `TGocciaEngine`) and pass it to `WriteSourceMapIfEnabled` — do not reject `--source-map` in interpreted mode.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 200
File: REPL.dpr:0-0
Timestamp: 2026-04-07T17:12:07.324Z
Learning: When reviewing timing/metrics code in the GocciaScript repo (e.g., REPL.dpr, ScriptLoader, TestRunner, BenchmarkRunner, and TGocciaEngine.Execute), follow the established timing convention: capture `StartTime` before the JSX pre-pass (`TGocciaJSXTransformer.Transform`) and `StringListToLFText`, then capture `LexEnd` after `ScanTokens`. The `Lex` duration (`LexEnd - StartTime`) is intended to include the JSX pre-pass because it is treated as part of the source→tokens pipeline. Do not flag this ordering as a bug and do not recommend moving `StartTime` to after the JSX transform, as it would break the bucket semantics.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: ScriptLoader.dpr:727-740
Timestamp: 2026-04-14T13:51:07.273Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), the `--emit` flag (bytecode compilation and output to a `.gbc` file) is an exploratory/temporary feature added to validate the bytecode pipeline. It is NOT the long-term solution for bytecode output. A proper bundling mechanism is being designed separately and tracked in issue `#156`. Do not flag `--emit` multi-file overwrite behavior or the presence of bytecode emission logic in ScriptLoader as architectural problems — the entire emit path is expected to be replaced by the bundler. ScriptLoader doing bytecode output is acknowledged as "too much responsibility" by the maintainer.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:211-212
Timestamp: 2026-03-23T12:37:30.936Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas (PR `#107`)
Learning: In TGocciaRuntimeOperations.MarkExternalRoots, entries in FModuleCache (cached module namespace/JSON records) must be traversed and their TSouffleValue references marked as external roots to survive GC. This behavior was added in commit 36f69cc; do not re‑flag missing FModuleCache rooting unless the logic regresses.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 209
File: units/Goccia.FFI.DynamicLibrary.pas:56-63
Timestamp: 2026-04-08T13:55:35.057Z
Learning: In frostney/GocciaScript (units/Goccia.Values.FFILibrary.pas, units/Goccia.FFI.DynamicLibrary.pas), `TGocciaFFIBoundFunction.Call` intentionally does not check whether the owning `TGocciaFFILibraryHandle` is closed before invoking `FFIDispatchCall(FSymbol, ...)`. Bound functions capture a raw `CodePointer` at bind-time; after `Close()` unloads the module, those pointers become dangling. This is an acknowledged v1 limitation — the library handle lifetime vs. bound-function lifetime is not enforced in v1. Do not re-flag this use-after-close hazard until a v2 tracking issue is opened and addressed.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-08T04:38:51.694Z
Learning: Applies to **/*.pas : Use `Goccia.FileExtensions` constants (`EXT_JS`, `EXT_JSX`, `EXT_TS`, `EXT_TSX`, `EXT_MJS`, `EXT_JSON`, `EXT_SBC`) instead of hardcoded string literals. Use `ScriptExtensions` array, `IsScriptExtension`, and `IsJSXNativeExtension` helpers instead of duplicating extension lists.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 147
File: units/Goccia.VM.pas:3042-3084
Timestamp: 2026-03-31T18:38:23.367Z
Learning: Repo: frostney/GocciaScript — units/Goccia.VM.pas (PR `#147`)
As of commit 457e712: (1) TGocciaVM.GetPropertyValue performs own-only private static accessor lookup on '#…' for TGocciaClassValue and throws TypeError on class-brand misses; (2) TGocciaVM.SetPropertyValue avoids inherited private accessor resolution, but still allows raw private static writes via SetRawPrivateValue to support current class initialization that emits OP_SET_PROP_CONST for private static fields. Planned follow-up is to add dedicated opcodes to define private static fields at class-declaration time and then make SetPropertyValue strict (throw on brand miss with no fallback). Do not re-flag the current fallback until those opcodes exist.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to **/*.{pas,dpr} : Use file extension constants from `Goccia.FileExtensions` (`EXT_JS`, `EXT_JSX`, `EXT_TS`, `EXT_TSX`, `EXT_MJS`, `EXT_JSON`) instead of hardcoded string literals. Use the `ScriptExtensions` array, `IsScriptExtension`, and `IsJSXNativeExtension` helpers instead of duplicating extension lists or ad-hoc checks.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/ScopeMap.pas:0-0
Timestamp: 2026-03-17T22:18:02.668Z
Learning: Repo: frostney/GocciaScript — ScopeMap.pas (units/ScopeMap.pas) was deleted (commit d99cf96) after profiling showed TScopeMap's linear-scan IndexOf was ~2.7× slower than hash-based lookup (CreateBridgedContext samples rose from 24% to 51%). Scope bindings now use TOrderedStringMap<TLexicalBinding> (DJB2 hash, O(1) per level); scope chain walking is done via recursive TGocciaScope-level methods (Contains/GetLexicalBinding) rather than a parent-pointer chain inside the map. Do not flag TScopeMap or its Parent property in future reviews — the type no longer exists.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 286
File: docs/testing.md:774-776
Timestamp: 2026-04-12T15:44:39.321Z
Learning: In frostney/GocciaScript TestRunner.dpr, both interpreted (`RunGocciaScriptInterpreted` via `TGocciaEngine`) and bytecode (`RunGocciaScriptBytecode` via `TGocciaBytecodeBackend`) paths use the same globals set `[ggTestAssertions, ggFFI]`. `TGocciaBytecodeBackend.RegisterBuiltIns` accepts the same `TGocciaGlobalBuiltins` type. Documentation stating "creates a fresh `TGocciaEngine` with `[ggTestAssertions, ggFFI]`" is intentionally concise and accurate for both modes — do not flag it as incomplete for omitting the bytecode backend distinction.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 139
File: units/Goccia.ScriptLoader.JSON.Test.pas:1-113
Timestamp: 2026-03-30T13:02:33.857Z
Learning: In frostney/GocciaScript, CI workflows (`.github/workflows/ci.yml` and `.github/workflows/pr.yml`) use glob patterns (`build/Goccia.*.Test*` for artifact upload, `Goccia.*.Test` / `Goccia.*.Test.exe` for execution) to discover and run native Pascal test binaries automatically. Do not flag missing entries in a hard-coded test list — new `units/*.Test.pas` test programs are picked up automatically via these globs as of commit b63911d (PR `#139`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: units/Goccia.Runtime.Operations.pas:8694-8696
Timestamp: 2026-03-10T18:05:46.926Z
Learning: Repo: frostney/GocciaScript PR: 60 — units/Goccia.Runtime.Operations.pas: The GC rooting of FArrayBridgeReverse (marking values in MarkExternalRoots and keys in MarkWrappedGocciaValues) is intentional to preserve array identity across async/microtask boundaries (e.g., Promise.resolve(arr).then(v => v === arr)). Do not prune this map during normal clearing; per-file GC.Collect in runners bounds cross-file memory growth.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 222
File: .github/workflows/ci.yml:562-572
Timestamp: 2026-04-08T14:58:47.163Z
Learning: In frostney/GocciaScript, the `test262-conformance` CI job (`.github/workflows/ci.yml`) is intentionally non-blocking: it uses `|| true` on the runner step and `raise SystemExit(0)` when no report is produced, so it never fails the job. It is deliberately excluded from the `needs` list of both `artifacts` and `release` jobs. Do not flag the `|| true` or the `SystemExit(0)` as issues — the job is designed as a purely informational conformance-tracking signal.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 87
File: units/Goccia.Runtime.Operations.pas:3383-3387
Timestamp: 2026-03-17T16:28:05.526Z
Learning: Repo: frostney/GocciaScript — Follow-up tracking
Learning: Splitting the blueprint bridge caches (value bridge vs construct bridge) is tracked in issue `#99`. Until that lands, sharing FBlueprintBridgeCache between UnwrapToGocciaValue (BlueprintToClassValue) and Construct (ConvertBlueprintToClassValue) should not block PRs; reference `#99` instead of re-flagging.

Comment thread TestRunner.dpr
Comment on lines +56 to +57
threadvar
FStaticMembers: TArray<TGocciaMemberDefinition>;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 14, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Free Pascal, are managed threadvar variables such as dynamic arrays (TArray) finalized automatically when a thread exits?

💡 Result:

No, managed threadvar variables such as dynamic arrays (TArray) are not finalized automatically when a thread exits in Free Pascal. Dynamic arrays are managed types, automatically finalized when going out of scope in normal procedure contexts. However, threadvar variables are global and per-thread, not scoped to procedures. Thread termination via EndThread calls DoneThread, which finalizes RTL components including heap and explicitly calls the thread manager's ReleaseThreadVars to release threadvar memory blocks. This releases the storage for the threadvar variables but does not perform managed type finalization (e.g., reference count decrement for dynamic arrays). Evidence from FPC developer discussions confirms DoneThread finalizes RTL parts and releases threadvars, but no mention of calling Finalize on managed threadvar contents. Official docs on managed types describe finalization at scope exit, not threadvar cleanup. Thus, dynamic array contents leak unless manually finalized before thread exit (e.g., set to nil or Finalize(var)).

Citations:


🏁 Script executed:

# Find the file and show context around lines 56-57
fd -t f "GlobalPromise.pas" --exec wc -l {} \;

Repository: frostney/GocciaScript

Length of output: 110


🏁 Script executed:

# Show the file structure and threadvar usage
rg "threadvar|FStaticMembers" units/Goccia.Builtins.GlobalPromise.pas -B 3 -A 10

Repository: frostney/GocciaScript

Length of output: 1219


🏁 Script executed:

# Check for any cleanup/finalization patterns for FStaticMembers in the file
rg "FStaticMembers" units/Goccia.Builtins.GlobalPromise.pas -B 2 -A 5

Repository: frostney/GocciaScript

Length of output: 599


🏁 Script executed:

# Search other builtins for similar threadvar patterns
rg "threadvar.*FStaticMembers" units/Goccia.Builtins.*.pas

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Search more broadly for threadvar declarations with dynamic arrays
rg "threadvar" units/Goccia.Builtins.*.pas -A 2

Repository: frostney/GocciaScript

Length of output: 3642


🏁 Script executed:

# Check how many times Create is called and understand the lifecycle
rg "TGocciaGlobalPromise.Create|procedure TGocciaGlobalPromise.Create" units/Goccia.Builtins.GlobalPromise.pas -B 5 -A 30 | head -60

Repository: frostney/GocciaScript

Length of output: 1367


🏁 Script executed:

# Check if there's any cleanup handler registered for thread termination
rg "DoneThread|ReleaseThreadVars|thread.*cleanup|Finalize.*FStaticMembers" units/Goccia.Builtins.GlobalPromise.pas

Repository: frostney/GocciaScript

Length of output: 47


Managed threadvar dynamic arrays leak memory on thread exit without explicit finalization.

FStaticMembers: TArray<TGocciaMemberDefinition> (and the same pattern in 16+ other builtins: YAML, TOML, Math, Performance, JSONL, JSON5, JSON, GlobalURL, GlobalString, GlobalSymbol, GlobalReflect, GlobalRegExp, GlobalObject, GlobalNumber, GlobalFFI, GlobalArray, GlobalArrayBuffer, Console) is a managed type stored in thread-local storage. Free Pascal's thread manager does not automatically finalize managed threadvar contents when a thread exits; ReleaseThreadVars releases the memory block but skips reference-count decrements and deep finalization. This causes memory leaks across thread lifetimes.

Add explicit cleanup:

  • Either set FStaticMembers := nil; before thread exit (requires a finalization callback or thread-pool cleanup hook)
  • Or use a thread-safe singleton cache or instance-scoped field instead of threadvar

All 17 affected builtins must be remedied consistently.

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

In `@units/Goccia.Builtins.GlobalPromise.pas` around lines 56 - 57, The threadvar
FStaticMembers (and the same pattern in the other builtins) holds a managed
dynamic array and leaks because Free Pascal doesn't finalize managed threadvar
data on thread exit; fix by removing managed types from thread-local storage and
adding explicit cleanup: either (preferred) replace the threadvar FStaticMembers
with a thread-safe singleton/cache or instance-scoped field (e.g., a
TGocciaBuiltinCache that exposes GetStaticMembers per-thread) so managed arrays
are owned and finalized by a regular object, or (quick patch) register a
thread-exit cleanup hook that sets FStaticMembers := nil for each affected unit
(GlobalPromise/FStaticMembers and the other listed builtin units) before the
thread exits; apply the same consistent change to all 17 affected units.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid finding — FPC does not finalize managed threadvar contents on thread exit. In our usage, each worker thread is short-lived (processes a handful of files then exits), so the leaked TArray<TGocciaMemberDefinition> data per thread is small (a few hundred bytes per unit × ~30 units ≈ a few KB per thread). The GC-managed objects are already cleaned up by ShutdownThreadRuntime.

A proper fix would require either exposing finalization procedures from each of the ~30 affected units (the threadvars are in implementation sections, so not accessible externally) or a registration-based cleanup system. Deferred as a follow-up for when long-lived thread pools are introduced. For the current spawn-and-join model, the OS reclaims all process memory on exit.

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.

@frostney, understood — the trade-off is well-reasoned: a few KB of leaked TArray shell memory per short-lived worker thread is negligible for the current spawn-and-join model, and the GC-managed heap objects are already properly cleaned up by ShutdownThreadRuntime. The real fix (registration-based cleanup or per-unit finalization hooks) makes more sense to build when long-lived thread pools become a concern.

I'll record this so it doesn't get re-flagged in future reviews. Would you like me to open a follow-up issue to track the registration-based cleanup approach for when long-lived thread pools are introduced?


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: units/Goccia.Builtins.GlobalPromise.pas:56-57
Timestamp: 2026-04-15T06:26:30.835Z
Learning: In frostney/GocciaScript (PR `#306`), ~30 builtin units (e.g., Goccia.Builtins.GlobalPromise.pas, Goccia.Builtins.GlobalArray.pas, Goccia.Builtins.Math.pas, and ~27 others) use `threadvar FStaticMembers: TArray<TGocciaMemberDefinition>` in their `implementation` sections. FPC does NOT finalize managed threadvar contents on thread exit, so the TArray shell (~few hundred bytes per unit) leaks per worker thread. This is an accepted/deferred trade-off: (1) threads are short-lived spawn-and-join workers, (2) the GC-managed TGocciaMemberDefinition objects are cleaned up by ShutdownThreadRuntime, (3) OS reclaims all memory on process exit, (4) a proper fix requires a registration-based cleanup system or per-unit finalization hooks since the threadvars are in implementation sections. Do NOT re-flag this managed-threadvar leak until long-lived thread pools are introduced and a cleanup registry is in place.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 147
File: units/Goccia.VM.pas:3042-3078
Timestamp: 2026-03-31T17:08:45.603Z
Learning: Repo: frostney/GocciaScript — units/Goccia.VM.pas
Learning: Private accessor handling (including static) is enforced in TGocciaVM:
- In GetPropertyValue, when AKey starts with '#':
  - If AObject is TGocciaClassValue, use PrivatePropertyGetter/PrivatePropertySetter. Invoke the getter when present; if only a setter exists, throw TypeError "Private accessor #... was defined without a getter".
  - If AObject is TGocciaObjectValue, walk the prototype chain for an own accessor descriptor keyed by the private name; if an accessor exists without a getter, throw the same TypeError. Otherwise, fall back to TryGetRawPrivateValue.
- In SetPropertyValue, symmetric behavior applies:
  - For TGocciaClassValue, call the private setter when present; if only a getter exists, throw TypeError "Private accessor #... was defined without a setter".
  - For instances, walk the prototype chain; if an accessor descriptor exists without a setter, throw; otherwise SetRawPrivateValue.
This ensures static and instance private accessors round-trip correctly and error on getter-only/setter-only cases.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 147
File: units/Goccia.VM.pas:3042-3084
Timestamp: 2026-03-31T18:38:23.367Z
Learning: Repo: frostney/GocciaScript — units/Goccia.VM.pas (PR `#147`)
As of commit 457e712: (1) TGocciaVM.GetPropertyValue performs own-only private static accessor lookup on '#…' for TGocciaClassValue and throws TypeError on class-brand misses; (2) TGocciaVM.SetPropertyValue avoids inherited private accessor resolution, but still allows raw private static writes via SetRawPrivateValue to support current class initialization that emits OP_SET_PROP_CONST for private static fields. Planned follow-up is to add dedicated opcodes to define private static fields at class-declaration time and then make SetPropertyValue strict (throw on brand miss with no fallback). Do not re-flag the current fallback until those opcodes exist.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 58
File: units/Goccia.Runtime.Operations.pas:5959-6132
Timestamp: 2026-03-10T11:17:46.617Z
Learning: GocciaScript’s Promise statics are intentionally not species/subclass-aware. Interpreter-level implementations (PromiseAll, PromiseRace, PromiseAllSettled, PromiseAny, withResolvers, try, resolve, reject) construct plain TGocciaPromiseValue via WrapAsPromise and do not use the call receiver (AThisValue). The Souffle VM bridge mirrors this by not forwarding AReceiver and by constructing TGocciaPromiseValue directly where applicable. Do not flag missing receiver-based construction for Promise statics. This applies to units/Goccia.Runtime.Operations.pas and related Promise implementations; ensure consistency across related Pas files in the frostney/GocciaScript repository.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 63
File: units/Goccia.Runtime.Operations.pas:7458-7476
Timestamp: 2026-03-11T09:31:51.130Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas
Learning: NativeRecordHasOwnProperty for TSouffleRecord receivers intentionally treats accessor getters as own properties (checks Rec.Has(Key) or (Rec.HasGetters and Rec.Getters.Has(Key))) to mirror interpreter behavior; this ensures performance.hasOwnProperty('timeOrigin') returns true in bytecode mode.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 87
File: units/Goccia.Runtime.Operations.pas:0-0
Timestamp: 2026-03-17T16:07:07.425Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas
Learning: In TGocciaRuntimeOperations.Construct, the native blueprint instantiation fast-path must be gated by the root blueprint: walk Bp up to WalkBp (the chain root) and only take the native path if not FBlueprintSuperValues.ContainsKey(WalkBp). This prevents derived chains that ultimately extend a wrapped/non-blueprint super from running field initializers and delegate wiring early; those should fall through to the bridge path instead. The delegate wiring loop should iterate WalkBp and only assign Methods.Delegate := FVM.RecordDelegate at the true root.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 87
File: units/Goccia.Runtime.Operations.pas:3383-3387
Timestamp: 2026-03-17T16:08:41.070Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas
Maintainer preference: FBlueprintBridgeCache is intentionally shared between UnwrapToGocciaValue (which uses BlueprintToClassValue) and Construct (which uses ConvertBlueprintToClassValue); this pre‑existing pattern on main is acceptable and should not be re‑flagged in future reviews unless a functional bug is demonstrated.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-05T07:58:33.798Z
Learning: Applies to **/{Builtins,Engine}*.pas : Use Goccia.ObjectModel member-definition tables and RegisterMemberDefinitions instead of ad hoc RegisterNativeMethod blocks when adding prototype surfaces

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.pas : Singleton values (e.g., `UndefinedValue`, `TrueValue`, `NaNValue`, `SmallInt` cache) must be pinned via `TGocciaGarbageCollector.Instance.PinValue` during engine initialization (consolidated in `PinSingletons`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 58
File: units/Goccia.Runtime.Operations.pas:2791-2794
Timestamp: 2026-03-10T09:21:24.467Z
Learning: In frostney/GocciaScript, units/Goccia.Runtime.Operations.pas GetProperty uses delegate-first lookup for built-ins (String, Number, Map, Set, Promise): it queries the respective delegate (e.g., FPromiseDelegate for TGocciaPromiseValue instances; FPromiseStaticDelegate for the Promise constructor) and only falls back to the underlying Goccia object/property lookup if the delegate doesn’t have the key. Built-in prototype methods on shared prototypes are immutable in GocciaScript and are not meant to be shadowed or reassigned; the delegate-first pattern is by design.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.pas : Values held only by Pascal code (not in any GocciaScript scope) must be protected with `AddTempRoot`/`RemoveTempRoot` for the duration they are needed.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: units/Goccia.Runtime.Operations.pas:8694-8696
Timestamp: 2026-03-10T18:05:46.926Z
Learning: Repo: frostney/GocciaScript PR: 60 — units/Goccia.Runtime.Operations.pas: The GC rooting of FArrayBridgeReverse (marking values in MarkExternalRoots and keys in MarkWrappedGocciaValues) is intentional to preserve array identity across async/microtask boundaries (e.g., Promise.resolve(arr).then(v => v === arr)). Do not prune this map during normal clearing; per-file GC.Collect in runners bounds cross-file memory growth.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 271
File: units/Goccia.Builtins.GlobalObject.pas:104-129
Timestamp: 2026-04-11T19:43:06.627Z
Learning: In frostney/GocciaScript, `TGarbageCollector.AddTempRoot`/`RemoveTempRoot` are set-membership (not reference-counted). In built-in method implementations, ONLY call `AddTempRoot`/`RemoveTempRoot` if the value was freshly boxed by `ToObject` (i.e., the argument was not already a `TGocciaObjectValue`). Do not unconditionally add/remove temp roots for values passed in as method arguments, since a single `RemoveTempRoot(Obj)` will remove the root for all holders and can cause use-after-free on the next GC cycle. Pattern: `if Assigned(TGarbageCollector.Instance) and not (AArgs.GetElement(0) is TGocciaObjectValue) then AddTempRoot(Obj); try ... finally if Assigned(TGarbageCollector.Instance) and not (AArgs.GetElement(0) is TGocciaObjectValue) then RemoveTempRoot(Obj); end;`

Learnt from: frostney
Repo: frostney/GocciaScript PR: 209
File: units/Goccia.Values.FFILibrary.pas:73-82
Timestamp: 2026-04-08T13:55:31.119Z
Learning: In frostney/GocciaScript (units/Goccia.Values.FFILibrary.pas, PR `#209`), `TGocciaFFIBoundFunction` intentionally holds only a raw `CodePointer` with no back-reference to the owning `TGocciaFFILibraryHandle`. Calling `close()` while bound functions or raw symbol pointers still exist is documented UB — the same contract as Deno/Bun FFI. This is an explicit v1 design decision, gated behind the `ggFFI` opt-in flag so only embedders who accept the contract enable it. A ref-counted handle guard is planned as a v2 enhancement. Do NOT re-flag the missing library back-reference or "bind() leaks" in future reviews of this unit.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: units/Goccia.Runtime.Operations.pas:3631-3642
Timestamp: 2026-03-10T21:38:23.719Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas
In TGocciaRuntimeOperations.GetIterator and FinalizeEnum, AddTempRoot is intentionally not balanced with RemoveTempRoot. Newly created TGocciaIteratorValue/TGocciaEnumValue instances are immediately wrapped via WrapGocciaValue and stored in FWrappedValues; TGocciaRuntimeOperations registers MarkWrappedGocciaValues as an external root marker so the underlying TGocciaValue is traced during GC. The temp root only protects the pre-wrap window; cleaning it up is a separate lifetime refactor and out of scope for PR `#60`.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 58
File: units/Goccia.Values.PromiseValue.pas:457-472
Timestamp: 2026-03-10T09:20:08.105Z
Learning: In `units/Goccia.Values.PromiseValue.pas`: `TGocciaPromiseFinallyWrapper` instances (`WrapFulfilled`, `WrapRejected`) created in `TGocciaPromiseValue.InvokeFinally` (and the original `PromiseFinally`) do NOT require explicit `PinValue` calls. They survive GC because: (1) all `TGocciaValue` instances auto-register with the GC via `AfterConstruction`; (2) the `TGocciaNativeFunctionValue` handlers wrapping their `Invoke` methods are stored in promise reactions (`FReactions`) or microtask queue entries; (3) `TGocciaPromiseValue.MarkReferences` marks all reactions and their `OnFulfilled`/`OnRejected` handlers. Do NOT flag missing pinning on these wrapper objects as a GC issue.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:211-212
Timestamp: 2026-03-23T12:37:30.936Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas (PR `#107`)
Learning: In TGocciaRuntimeOperations.MarkExternalRoots, entries in FModuleCache (cached module namespace/JSON records) must be traversed and their TSouffleValue references marked as external roots to survive GC. This behavior was added in commit 36f69cc; do not re‑flag missing FModuleCache rooting unless the logic regresses.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: units/GarbageCollector.Generic.pas:115-123
Timestamp: 2026-03-10T17:59:00.467Z
Learning: In units/GarbageCollector.Generic.pas (TGarbageCollector.Destroy): FManagedObjects is intentionally created with OwnsObjects = False and the destructor does NOT iterate and free remaining managed objects. This is pre-existing behavior (carried over from TGocciaGarbageCollector). Shutdown is only called at process exit (OS reclaims memory) or between engine instances (engine destructor chain frees values via scope teardown). Objects still present at shutdown are typically pinned singletons with process lifetime. Do NOT flag missing per-object freeing in this destructor as a memory leak issue.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 209
File: units/Goccia.FFI.DynamicLibrary.pas:56-63
Timestamp: 2026-04-08T13:55:35.057Z
Learning: In frostney/GocciaScript (units/Goccia.Values.FFILibrary.pas, units/Goccia.FFI.DynamicLibrary.pas), `TGocciaFFIBoundFunction.Call` intentionally does not check whether the owning `TGocciaFFILibraryHandle` is closed before invoking `FFIDispatchCall(FSymbol, ...)`. Bound functions capture a raw `CodePointer` at bind-time; after `Close()` unloads the module, those pointers become dangling. This is an acknowledged v1 limitation — the library handle lifetime vs. bound-function lifetime is not enforced in v1. Do not re-flag this use-after-close hazard until a v2 tracking issue is opened and addressed.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Parser.pas:949-961
Timestamp: 2026-04-11T09:44:39.519Z
Learning: In frostney/GocciaScript (units/Goccia.AST.Node.pas, units/Goccia.AST.Statements.pas): `TGocciaProgram` has no destructor and does NOT free its `FBody: TObjectList<TGocciaStatement>` when `Free` is called. `TGocciaExpressionStatement` similarly has no destructor and does NOT free its `FExpression: TGocciaExpression`. Therefore, extracting a child expression from a temporary `TGocciaProgram` (e.g., via `TGocciaExpressionStatement(ProgramNode.Body[0]).Expression`) and then calling `ProgramNode.Free` is safe — the extracted expression is NOT freed and remains valid. This pattern is intentional and is used in `ParseTaggedTemplate` and similar parsing helpers. Do NOT flag this as a dangling-pointer or use-after-free issue.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 209
File: units/Goccia.Builtins.GlobalFFI.pas:0-0
Timestamp: 2026-04-08T15:02:36.765Z
Learning: In frostney/GocciaScript (units/Goccia.Builtins.GlobalFFI.pas, PR `#209`), `FFI.alloc` and `FFI.free` were removed in a later commit. User-owned FFI memory is now managed through `new ArrayBuffer(size)` and TypedArray views rather than a dedicated FFI allocation API. Do not flag `GetMem`/`EOutOfMemory` or `Double→Integer` overflow issues in `FFIAlloc`; the method no longer exists in the FFI surface.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: souffle/Souffle.VM.pas:96-104
Timestamp: 2026-03-10T17:58:57.247Z
Learning: In souffle/Souffle.VM.pas, TSouffleVM caches TGarbageCollector.Instance in FGC during construction. This is safe because TGocciaSouffleBackend.Destroy always frees the VM (TSouffleVM.Destroy) before calling GC.Shutdown. The enforced lifecycle order is: GC.Initialize → VM.Create → ... → VM.Destroy → Backend.Destroy → GC.Shutdown. Do NOT flag the cached FGC field as a dangling-pointer risk — re-querying TGarbageCollector.Instance in the destructor would add overhead without addressing an actual bug.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 45
File: units/Goccia.ControlFlow.pas:13-18
Timestamp: 2026-03-07T23:37:29.010Z
Learning: In `units/Goccia.ControlFlow.pas`: `TGocciaControlFlow.Value` (a `TGocciaValue` held in a plain record) is intentionally left unrooted. CF propagation is synchronous — no GC-triggering allocations occur between production and consumption of the value — so there is no live-value collection window. Do NOT flag missing `AddTempRoot`/`RemoveTempRoot` on `TGocciaControlFlow` fields; adding managed-record operators (Initialize/Finalize) would add per-creation/destruction overhead that contradicts the performance goal of this refactoring (replacing exception-based control flow with lightweight records).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/ScopeMap.pas:0-0
Timestamp: 2026-03-17T22:18:02.668Z
Learning: Repo: frostney/GocciaScript — ScopeMap.pas (units/ScopeMap.pas) was deleted (commit d99cf96) after profiling showed TScopeMap's linear-scan IndexOf was ~2.7× slower than hash-based lookup (CreateBridgedContext samples rose from 24% to 51%). Scope bindings now use TOrderedStringMap<TLexicalBinding> (DJB2 hash, O(1) per level); scope chain walking is done via recursive TGocciaScope-level methods (Contains/GetLexicalBinding) rather than a parent-pointer chain inside the map. Do not flag TScopeMap or its Parent property in future reviews — the type no longer exists.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.pas : `type`/`interface` declarations and `import type`/`export type` must produce `TGocciaEmptyStatement` (no-op at runtime). Access modifiers (`public`, `protected`, `private`, `readonly`, `override`, `abstract`) in class bodies must be consumed and discarded.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: ScriptLoader.dpr:692-707
Timestamp: 2026-04-15T06:26:10.118Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `TScriptLoaderApp.ScriptWorkerProc` intentionally calls `RunScriptFromFile` directly in worker threads, which causes interleaved/nondeterministic stdout in parallel directory runs. This is an accepted trade-off for the initial parallel implementation — ScriptLoader parallel mode is only used for directory inputs where output ordering is less critical. A proper fix (refactoring `RunSource`/`ExecuteInterpreted` to support output capture and returning text via `AConsoleOutput`/`AErrorMessage`) has been deferred to a follow-up. Do not re-flag the direct `RunScriptFromFile` call in `ScriptWorkerProc` as a regression or issue until the output-capture refactor lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 139
File: units/Goccia.ScriptLoader.JSON.Test.pas:1-113
Timestamp: 2026-03-30T13:02:33.857Z
Learning: In frostney/GocciaScript, CI workflows (`.github/workflows/ci.yml` and `.github/workflows/pr.yml`) use glob patterns (`build/Goccia.*.Test*` for artifact upload, `Goccia.*.Test` / `Goccia.*.Test.exe` for execution) to discover and run native Pascal test binaries automatically. Do not flag missing entries in a hard-coded test list — new `units/*.Test.pas` test programs are picked up automatically via these globs as of commit b63911d (PR `#139`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: ScriptLoader.dpr:724-737
Timestamp: 2026-04-14T06:46:14.795Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `--emit` with `--output=<file>` and multiple input paths or a directory input silently overwrites the same output file for each compiled module. This is pre-existing behavior preserved across the CLI refactor in PR `#299` and should NOT be re-flagged as a regression. A follow-up guard (raising TGocciaParseError when FEmit.Present and FOutputPath.Present and APaths.Count > 1 or the single path is a directory) was proposed but deferred.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T13:53:18.321Z
Learning: In frostney/GocciaScript CLI tool sources, the duplicated `EscapeJSONString` helper (escaping only `\`, `"`, `\n`, `\r`, `\t`, leaving other C0 control characters U+0000–U+001F unescaped) is tracked for consolidation into a shared helper and full fix in issue `#303`. During code review, do not re-flag missing control-character escaping in individual per-tool `EscapeJSONString` copies (e.g., TestRunner.dpr, BenchmarkRunner.dpr) until the consolidation PR landing issue `#303` is resolved.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 289
File: ScriptLoader.dpr:140-156
Timestamp: 2026-04-12T21:19:18.811Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr, TestRunner.dpr, BenchmarkRunner.dpr): suppressing status/progress WriteLn messages in JSON output mode is intentionally handled per-tool for now. The long-term goal is to unify the stdout/JSON-output suppression pattern across all CLI tools. In ScriptLoader, status lines must be gated on `not GJsonOutput` because `--output=json` routes the result document directly to stdout. TestRunner and BenchmarkRunner write JSON to a file, so their current unconditional WriteLn to stdout is safe but will need to align with the unified pattern in a future refactor.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 200
File: REPL.dpr:0-0
Timestamp: 2026-04-07T17:12:07.324Z
Learning: When reviewing timing/metrics code in the GocciaScript repo (e.g., REPL.dpr, ScriptLoader, TestRunner, BenchmarkRunner, and TGocciaEngine.Execute), follow the established timing convention: capture `StartTime` before the JSX pre-pass (`TGocciaJSXTransformer.Transform`) and `StringListToLFText`, then capture `LexEnd` after `ScanTokens`. The `Lex` duration (`LexEnd - StartTime`) is intended to include the JSX pre-pass because it is treated as part of the source→tokens pipeline. Do not flag this ordering as a bug and do not recommend moving `StartTime` to after the JSX transform, as it would break the bucket semantics.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: TestRunner.dpr:237-240
Timestamp: 2026-04-07T20:24:01.235Z
Learning: Repo: frostney/GocciaScript — TestRunner.dpr / Goccia.Coverage
The synthetic `runTests({...})` call is appended to `Source` before lexing in both `RunGocciaScriptBytecode` and `RunGocciaScriptInterpreted`. `Lexer.SourceLines` therefore includes this line, and `CountExecutableLines` counts it as +1 executable. Because the engine always executes it, coverage also records +1 hit. The net effect on the percentage is self-consistent (+1/+1) and does NOT skew coverage ratios. `PrintCoverageDetail` reads the original file from disk, so the synthetic line never appears in the detail output. Do not flag this as a coverage accuracy bug.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 222
File: .github/workflows/ci.yml:562-572
Timestamp: 2026-04-08T14:58:47.163Z
Learning: In frostney/GocciaScript, the `test262-conformance` CI job (`.github/workflows/ci.yml`) is intentionally non-blocking: it uses `|| true` on the runner step and `raise SystemExit(0)` when no report is produced, so it never fails the job. It is deliberately excluded from the `needs` list of both `artifacts` and `release` jobs. Do not flag the `|| true` or the `SystemExit(0)` as issues — the job is designed as a purely informational conformance-tracking signal.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 53
File: souffle/Souffle.Wasm.Emitter.pas:1467-1477
Timestamp: 2026-03-09T17:21:38.193Z
Learning: In this codebase (Frostney/GocciaScript), for Pascal/Delphi (FPC) files using AnsiString with {$mode delphi} {H+}, treat String as byte-indexed. Length(str) returns byte count and Byte(str[I]) is a no-op cast. Do not replace per-byte copy loops like Data[I-1] := Byte(AStr[I]) with TEncoding.GetBytes, since TEncoding is a Delphi RTL feature unavailable in FPC's SysUtils and would break the build. Apply this guidance to all .pas files under the project; ensure manual per-byte copying logic remains correct for UTF-8 content encoded in AnsiString, and avoid introducing RTL dependencies not supported by FPC.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 63
File: units/Goccia.Runtime.Operations.pas:0-0
Timestamp: 2026-03-11T01:50:11.071Z
Learning: Replace hardcoded string literals 'now', 'toJSON', and 'timeOrigin' with the new constants from Goccia.Constants.PropertyNames (PROP_NOW, PROP_TO_JSON, PROP_TIME_ORIGIN). Apply this refactor across the repository wherever these literals are used, including but not limited to units/Goccia.Runtime.Operations.pas and units/Goccia.Builtins.Performance.pas. Use a project-wide search (e.g., regex or code navigation) to identify all occurrences, update to the respective constants, and adjust imports or uses accordingly. Ensure all references compile and behavior remains unchanged.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 63
File: units/Goccia.Runtime.Operations.pas:7514-7544
Timestamp: 2026-03-11T08:22:20.482Z
Learning: When bridging a native namespace object to a TSouffleRecord (e.g., performance) in units/Goccia.Runtime.Operations.pas, always set Result.Delegate := ARuntime.VM.RecordDelegate after TSouffleRecord.Create(...). This ensures Object.prototype methods (toString/hasOwnProperty/valueOf/toLocaleString) work in bytecode mode and NativeRecordToString honors Symbol.toStringTag. Apply this pattern to BuildPerformanceRecord and any future bridged records.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/HashMap.pas:266-309
Timestamp: 2026-03-17T22:17:37.410Z
Learning: Do not flag enumeration in THashMap<TKey, TValue> as concurrent-modification unsafe. The no-mutation contract is maintained during enumeration: GetEnumerator shares the internal FSlots array (no per-call allocation) and only mutation occurs via destructor or Clear, not during active iteration. This pattern should be recognized across similar Pascal units/pascal collection implementations; when reviewing code, ensure no concurrent-modification warnings are raised for shared internal structures during enumeration if the no-mutation contract is preserved.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/OrderedMap.pas:0-0
Timestamp: 2026-03-17T22:17:55.588Z
Learning: Enforce the constraint on TOrderedMap<TKey, TValue> in units/OrderedMap.pas: TKey must be an unmanaged, fixed-size value type (no strings, dynamic arrays, or records with managed/pointer fields). HashKey should use fast paths for SizeOf(TKey)=8 (QWord) and SizeOf(TKey)=4 (Cardinal); KeysEqual should compare via direct PQWord/PCardinal for those sizes. The raw-byte DJB2/CompareMem fallback is dead code for current uses. Document this constraint in a unit-header comment as the enforcement mechanism. Do NOT re-flag the byte-wise hash/equality in future reviews as long as all TOrderedMap specializations use pointer-sized unmanaged keys.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/ScopeMap.pas:0-0
Timestamp: 2026-03-17T22:18:00.984Z
Learning: Do not re-introduce TScopeMap. In frostney/GocciaScript, TScopeMap<TValue> was removed due to performance regressions from its linear-scan Add-as-upsert behavior. Reviews should flag any use of TScopeMap in Units/ScopeMap.pas or elsewhere and ensure bindings use TOrderedStringMap<TLexicalBinding> instead. If reviewing similar Pascal files, prefer avoiding upsert-like add semantics that overwrite existing entries, as they correlated with bytecode performance drops. Ensure changes adhere to the established replacement: TOrderedStringMap<TLexicalBinding>.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/BaseMap.pas:22-22
Timestamp: 2026-03-18T08:12:24.165Z
Learning: Shared/utility units under the repository (e.g., units/BaseMap.pas, HashMap.pas, OrderedMap.pas, etc.) that are used across multiple parts of the project should not be required to use the Goccia.* prefix. A separate naming convention will be defined in a follow-up issue. Do not flag missing Goccia.* prefix for these shared infrastructure units in reviews.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/BaseMap.pas:22-22
Timestamp: 2026-03-18T08:12:24.165Z
Learning: Do not flag missing Goccia.* prefixes for shared/utility units that are used across multiple parts of the project or can be extracted as standalone infrastructure (e.g., BaseMap.pas, HashMap.pas, OrderedMap.pas, OrderedStringMap.pas, GC, test assertion units). A separate naming convention (to be defined in issue `#104`) should apply to these targets. When reviewing, apply this guideline to all similar shared utility units under the units directory; for one-off implementations outside this scope, continue with the existing naming expectations.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 110
File: units/Goccia.Runtime.Operations.pas:0-0
Timestamp: 2026-03-23T11:25:00.381Z
Learning: In this repo’s GocciaScript Pascal code, when implementing thenable detection/callability checks (e.g., around Await/unwrap logic), always read the thenable’s `then` property using `Goccia.Constants.PropertyNames.PROP_THEN` rather than a string literal. If the value being inspected is a `TSouffleRecord`, unwrap the retrieved `then` property via `UnwrapToGocciaValue` before testing whether it is callable (e.g., in `TGocciaRuntimeOperations.AwaitValue`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:4349-4353
Timestamp: 2026-03-23T12:38:54.412Z
Learning: In Pascal source under units/, when decoding JSON string escape sequences that represent Unicode code points and then converting them to UTF-8, prefer using the existing helper (implemented via CodePointToUTF8) that correctly combines UTF-16 surrogate pairs (\uD800–\uDBFF + \uDC00–\uDFFF) into a single Unicode code point before UTF-8 encoding. Avoid ad-hoc conversions like Char(StrToInt(...)) for code-point→UTF-8 conversion logic; use the helper instead to ensure surrogate pairs are handled correctly.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 109
File: units/Goccia.Runtime.Operations.pas:6337-6344
Timestamp: 2026-03-23T15:57:55.802Z
Learning: In Pascal unit files under units/, any iterator result record objects produced by native iterators (e.g., in routines analogous to NativeMapIteratorNext / NativeSetIteratorNext) must: (1) set `Rec.Delegate := FVM.RecordDelegate`; and (2) set properties using `PROP_VALUE` / `PROP_DONE` constants from `Goccia.Constants.PropertyNames` rather than hard-coded string literals. Apply the same pattern to any future native iterator result objects so they interoperate correctly in bytecode mode (e.g., for `Object.prototype` method behavior) and keep property-name usage consistent via the shared constants.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 136
File: docs/adding-built-in-types.md:105-133
Timestamp: 2026-03-27T14:09:25.371Z
Learning: In frostney/GocciaScript Pascal code, do not flag or suggest refactoring the early-exit singleton initialization guard `if Assigned(FShared) then Exit` when it appears at the top of `InitializePrototype`. The project documentation treats this early-exit form as the canonical/shared-prototype singleton initialization template (i.e., avoid recommending an `if not Assigned(FShared) then ...` wrapping alternative).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 189
File: units/Goccia.JSON.pas:499-526
Timestamp: 2026-04-05T17:33:32.709Z
Learning: In frostney/GocciaScript, when using `TStringBuffer` from the `StringBuffer` unit, treat it as an advanced record (stack-allocated) with automatic cleanup by the compiler. For `TStringBuffer` locals, do not require or flag missing `SB.Free` calls in `try/finally` blocks—no destructor/free call is needed or appropriate. Only look for `Free` when the variable is actually a heap-allocated class instance.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 189
File: units/Goccia.JSON.pas:535-597
Timestamp: 2026-04-05T17:33:35.381Z
Learning: When reviewing Free Pascal code in this repo, treat `TStringBuffer` (from the `StringBuffer` unit) as an advanced record (value type), meaning instances are stack-allocated. Do not report or require `SB.Free` / `SB.FreeAndNil` (or any `Free` call) for variables typed as `TStringBuffer`, and do not treat an empty or no-op `finally` block after `TStringBuffer` usage as an issue. Only expect heap-management calls for actual `class`/reference types; calling `Free` on an advanced record is incorrect.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: units/Goccia.AST.Expressions.pas:1339-1346
Timestamp: 2026-04-07T21:00:57.707Z
Learning: When reviewing GocciaScript coverage/branch tracking, do not require call-sites to explicitly pre-register binary (two-arm) branches before recording hits. In units/Goccia.Coverage.pas, TGocciaFileCoverage.RecordBranchHit automatically ensures the opposite arm exists via EnsureBranchExists(ALine, AColumn, 1 - ABranchIndex), establishing a zero-hit baseline for the paired arm whenever either arm is first recorded. Therefore, call-sites such as TGocciaConditionalExpression.Evaluate (Goccia.AST.Expressions.pas), EvaluateIf/EvaluateBinary/EvaluateSwitch (Goccia.Evaluator.pas), and OP_JUMP_IF_* handlers (Goccia.VM.pas) should not be flagged for missing a separate RegisterBranch call for binary branches—pairing is handled inside RecordBranchHit.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: units/Goccia.VM.pas:3547-3561
Timestamp: 2026-04-07T21:01:16.129Z
Learning: When reviewing calls that record branch coverage via `TGocciaFileCoverage.RecordBranchHit(ALine, AColumn, ABranchIndex)` for binary branches (branch indices 0 and 1), do not require an explicit prior `RegisterBranch(...)` at the call site. `RecordBranchHit` internally calls `EnsureBranchExists(ALine, AColumn, 1 - ABranchIndex)` for binary branches, which guarantees both arms exist in the branch list (the untaken arm is pre-registered with zero hits). Therefore VM/evaluator/AST branch recording sites in `Goccia.VM.pas`, `Goccia.Evaluator.pas`, and `Goccia.AST.Expressions.pas` should not be flagged for missing explicit pre-registration before `RecordBranchHit(...)`.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 209
File: units/Goccia.FFI.DynamicLibrary.pas:7-21
Timestamp: 2026-04-08T13:56:59.262Z
Learning: In Free Pascal (FPC), `TLibHandle` is defined in the `System` unit (`TLibHandle = PtrInt`), not in `Dynlibs`. Since `System` is implicitly available in all Pascal units, referencing `TLibHandle` in the interface section (e.g., as a private field type of a class declared in the interface/`interface` section) does not require `Dynlibs` to be listed in the interface `uses` clause. Only ensure `Dynlibs` is included in the implementation `uses` clause when you actually use `LoadLibrary`, `GetProcAddress`, or `FreeLibrary`. Therefore, do not flag a missing `Dynlibs` entry in the interface `uses` clause when the interface section references only `TLibHandle`.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 242
File: AGENTS.md:396-400
Timestamp: 2026-04-09T11:50:01.452Z
Learning: In .pas code, avoid unsafe Int64→Double conversions/workarounds that trigger FPC 3.2.2 miscompilations. Specifically, do not rely on `Double(Int64Var)` when it performs bit reinterpretation (Delphi-mode front-end bug on affected versions/platforms). Also do not use arithmetic patterns like `Int64Var * 1.0` / `Int64Var * Double` that can produce incorrect results near ±2^31 on AArch64. Prefer value-safe conversions via implicit assignment (e.g., `var D: Double; D := SomeInt64`) or implicit promotion through function/constructor parameter passing (e.g., `TGocciaNumberLiteralValue.Create(SomeInt64)`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 253
File: units/Goccia.Values.Iterator.Concat.pas:84-88
Timestamp: 2026-04-10T12:44:59.067Z
Learning: In frostney/GocciaScript (GocciaScript), `TGocciaFunctionBase(callable).Call(args, thisValue)` is the project-standard way to invoke callable values. During code review, do not flag `TGocciaFunctionBase(...)` casts at call sites as unsafe and do not recommend replacing them with an alternative “generic callable path”—for this codebase, this cast-and-`Call` pattern is the intended generic callable invocation mechanism.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 273
File: units/Goccia.Values.URLValue.pas:465-470
Timestamp: 2026-04-11T17:42:27.330Z
Learning: In this codebase, when implementing or updating a `TGCManagedObject.MarkReferences` override for a held reference, treat `MarkReferences` as the act of marking *and* tracing children. The expected pattern is: after a nil check, call the held reference’s `MarkReferences` implementation directly (e.g., `TGocciaSomeValue(FRef).MarkReferences`) rather than invoking any non-existent separate `Mark(obj)`/`Mark()` helper. Do not suggest “call `Mark(obj)`”; this project’s marking flow starts by setting `FGCMark := FCurrentMark` and then tracing child references from within `MarkReferences` (consistent with `TGocciaMapValue`, `TGocciaSetValue`, `TGocciaArrayValue`, etc.).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 278
File: units/Goccia.Parser.pas:1071-1111
Timestamp: 2026-04-12T07:39:20.695Z
Learning: Do not free the result of `TGocciaLexer.ScanTokens` (e.g., in `ParseInterpolationExpression` and any other caller). `ScanTokens` returns a reference to the lexer's internal `FTokens: TObjectList<TGocciaToken>` created with `OwnsObjects=True` and freed by `TGocciaLexer`’s destructor. Adding `Tokens.Free` (or otherwise freeing the returned list/object) will cause double-free/access violations; token lifetime is managed entirely by the `TGocciaLexer` instance.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 285
File: units/Goccia.Lexer.pas:207-213
Timestamp: 2026-04-12T15:46:28.683Z
Learning: In this Frostney/GocciaScript project (Free Pascal / FPC, including files under units/), do not recommend adding explicit `{$B-}` directives (either inside units or in `Goccia.inc`) to “fix” short-circuit evaluation safety. `{$B-}` is already FPC’s default in all compiler modes (`fpc`, `delphi`, `objfpc`). Treat missing `{$B-}` as expected and do not flag it as a maintainability or safety issue. Only note `{$B-}` overrides if an explicit `{$B+}` is present in the file or via a highly visible project-level `fpc.cfg` setting.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 296
File: units/Goccia.REPL.Formatter.pas:15-29
Timestamp: 2026-04-13T16:53:02.191Z
Learning: For Pascal units in frostney/GocciaScript, enforce `uses`-clause grouping order as follows (with blank lines between groups): (1) System units (e.g., `SysUtils`), (2) Third-party units, then (3) Project units (e.g., `Goccia.*`). Specifically, `StringBuffer` (from `StringBuffer.pas`) must be treated as a Third-party unit and placed in the Third-party group. Do not report a `uses` ordering violation when `StringBuffer` appears between the System group and the `Goccia.*` group, since `./format.pas` and the Lefthook pre-commit hook enforce this grouping.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 302
File: docs/contributing/code-style.md:31-37
Timestamp: 2026-04-14T13:36:54.107Z
Learning: When reviewing Free Pascal (FPC) sources in this project, do not flag the compiler directive `{H+}` as a syntax error or as an inconsistency caused by a missing `$`. In FPC, `{H+}` (short-form) is valid and equivalent to `{$H+}` for enabling long/AnsiStrings. This project’s own `Goccia.inc` and documentation intentionally use `{H+}`, so the reviewer should only consider it an issue if it’s not valid FPC syntax for the surrounding context.

Comment thread units/Goccia.Builtins.GlobalRegExp.pas
Comment thread units/Goccia.Coverage.pas
Comment thread units/Goccia.ObjectModel.Engine.pas Outdated
Comment thread units/Goccia.Threading.pas Outdated
Comment thread units/Goccia.Timeout.pas
Comment thread units/Goccia.Values.FunctionBase.pas
Comment thread units/Goccia.Values.SymbolValue.pas
- Run scripts, tests, and benchmarks with configurable jobs
- Update shared prototype setup for concurrent startup
- Document new `--jobs` / `-j` options and progress output
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
ScriptLoader.dpr (1)

715-730: Correct initialization and error aggregation pattern.

The parallel implementation correctly:

  • Initializes shared prototypes before spawning workers
  • Aggregates errors on the main thread using Pool.Results[I].ErrorMessage
  • Sets ExitCode := 1 when any script fails

However, the worker's captured errors only contain E.Message (line 703), losing the detailed formatting available for TGocciaError and TGocciaThrowValue exceptions.

💡 Consider preserving detailed error messages
 procedure TScriptLoaderApp.ScriptWorkerProc(const AFileName: string;
   const AIndex: Integer; out AConsoleOutput: string;
   out AErrorMessage: string; AData: Pointer);
 begin
   AConsoleOutput := '';
   AErrorMessage := '';
   try
     RunScriptFromFile(AFileName);
   except
-    on E: Exception do
+    on E: TGocciaError do
+    begin
+      AErrorMessage := E.GetDetailedMessage;
+      ExitCode := 1;
+    end;
+    on E: TGocciaThrowValue do
     begin
       AErrorMessage := E.Message;
       ExitCode := 1;
+    end;
+    on E: Exception do
+    begin
+      AErrorMessage := 'Fatal error: ' + E.Message;
+      ExitCode := 1;
     end;
   end;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScriptLoader.dpr` around lines 715 - 730, ScriptWorkerProc currently stores
only E.Message into Pool.Results[].ErrorMessage, which discards the rich
formatting available for TGocciaError and TGocciaThrowValue; update the
exception handler inside ScriptWorkerProc to detect those types (TGocciaError,
TGocciaThrowValue) and produce a detailed formatted string (using their
available formatting/to-string helpers or by extracting fields) before assigning
to Pool.Results[WorkerIndex].ErrorMessage so the main thread's error aggregation
sees full details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@BenchmarkRunner.dpr`:
- Around line 604-650: BenchmarkWorkerProc spawns worker threads that call
CollectBenchmarkFile which currently writes directly to StdErr on exceptions,
causing interleaved stderr output; modify the worker to capture any stderr
output and forward it via AErrorMessage instead of letting background threads
write to StdErr: wrap the CollectBenchmarkFile call in a temporary stderr
capture (e.g., redirect StdErr to a memory stream or string buffer) inside
TBenchmarkRunnerApp.BenchmarkWorkerProc around the call to
CollectBenchmarkFile(AFileName, WorkerReporter, Mode, False), then on exit or
exception append the captured stderr contents to AErrorMessage (along with
E.Message if an exception occurs) so no thread writes to global StdErr, or
alternatively change the
CollectBenchmarkFileInterpreted/CollectBenchmarkFileBytecode implementations to
return error text instead of writing to StdErr and have BenchmarkWorkerProc
assign that text to AErrorMessage.

In `@TestRunner.dpr`:
- Around line 634-636: RunGocciaScript (and its helpers
RunGocciaScriptInterpreted and RunGocciaScriptBytecode) currently call WriteLn
on worker threads; change these functions to capture any error messages into the
worker's return structure (e.g. add/populate an ErrorText field on FileResult or
TestResult) instead of calling WriteLn at the error sites (the current WriteLn
calls around lines noted in RunGocciaScriptInterpreted and
RunGocciaScriptBytecode). Remove the WriteLn invocations from those functions,
ensure RunGocciaScript returns the error text alongside TestResult, and update
the main-thread aggregation code that handles FileResult/TestResult to print the
collected ErrorText to stdout so all console output happens on the main thread.

---

Nitpick comments:
In `@ScriptLoader.dpr`:
- Around line 715-730: ScriptWorkerProc currently stores only E.Message into
Pool.Results[].ErrorMessage, which discards the rich formatting available for
TGocciaError and TGocciaThrowValue; update the exception handler inside
ScriptWorkerProc to detect those types (TGocciaError, TGocciaThrowValue) and
produce a detailed formatted string (using their available formatting/to-string
helpers or by extracting fields) before assigning to
Pool.Results[WorkerIndex].ErrorMessage so the main thread's error aggregation
sees full details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61a5c9e5-4626-46cf-a01b-896475b0bca2

📥 Commits

Reviewing files that changed from the base of the PR and between c00e6a4 and 7916c07.

📒 Files selected for processing (9)
  • AGENTS.md
  • BenchmarkRunner.dpr
  • README.md
  • ScriptLoader.dpr
  • TestRunner.dpr
  • docs/benchmarks.md
  • docs/build-system.md
  • docs/testing.md
  • units/Goccia.Values.ArrayValue.pas
✅ Files skipped from review due to trivial changes (5)
  • docs/benchmarks.md
  • README.md
  • docs/build-system.md
  • AGENTS.md
  • docs/testing.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • units/Goccia.Values.ArrayValue.pas

Comment thread BenchmarkRunner.dpr
Comment thread TestRunner.dpr
Comment on lines +634 to +636
try
FileResult := RunGocciaScript(AFileName);
TestResult := FileResult.TestResult;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 15, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Worker still invokes methods that write to stdout from background threads.

RunGocciaScript (via RunGocciaScriptInterpreted/RunGocciaScriptBytecode) contains WriteLn calls for errors (lines 321, 326, 332 in interpreted; 465, 470, 476 in bytecode). These execute on worker threads, causing nondeterministic output ordering.

Consider capturing error output in the worker data record instead of writing directly, then printing on the main thread during result aggregation.

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

In `@TestRunner.dpr` around lines 634 - 636, RunGocciaScript (and its helpers
RunGocciaScriptInterpreted and RunGocciaScriptBytecode) currently call WriteLn
on worker threads; change these functions to capture any error messages into the
worker's return structure (e.g. add/populate an ErrorText field on FileResult or
TestResult) instead of calling WriteLn at the error sites (the current WriteLn
calls around lines noted in RunGocciaScriptInterpreted and
RunGocciaScriptBytecode). Remove the WriteLn invocations from those functions,
ensure RunGocciaScript returns the error text alongside TestResult, and update
the main-thread aggregation code that handles FileResult/TestResult to print the
collected ErrorText to stdout so all console output happens on the main thread.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same situation as the ScriptLoader and BenchmarkRunner stdout comments — the WriteLn calls in RunGocciaScriptInterpreted/RunGocciaScriptBytecode are in error-handling paths that write error details. In practice, these are rare (only on script load failures or uncaught exceptions) and the messages include filenames so they're identifiable even if interleaved. A full output-capture refactor of the execution chain is deferred as a follow-up.

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.

@frostney, understood — same accepted trade-off as in ScriptLoader and BenchmarkRunner. The WriteLn calls in the error-handling paths of RunGocciaScriptInterpreted / RunGocciaScriptBytecode are rare and filename-prefixed, so interleaving is tolerable for now. I'll note this to avoid re-flagging it until a full output-capture refactor lands.


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: TestRunner.dpr:634-636
Timestamp: 2026-04-15T06:27:53.198Z
Learning: In frostney/GocciaScript (PR `#306`, TestRunner.dpr), the `WriteLn` calls inside the error-handling paths of `RunGocciaScriptInterpreted` and `RunGocciaScriptBytecode` (e.g. on EStreamError, TGocciaError, TGocciaThrowValue, and generic Exception) execute on worker threads during parallel directory runs, causing potentially interleaved stdout. This is an accepted trade-off: these paths are rare (script-load failures / uncaught exceptions only), messages include filenames for identification, and a proper output-capture refactor (routing error text through AConsoleOutput/AErrorMessage and printing on the main thread) is deferred to a follow-up. Do NOT re-flag these WriteLn calls in TestRunner.dpr parallel worker context until the output-capture refactor lands.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: ScriptLoader.dpr:692-707
Timestamp: 2026-04-15T06:26:10.118Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `TScriptLoaderApp.ScriptWorkerProc` intentionally calls `RunScriptFromFile` directly in worker threads, which causes interleaved/nondeterministic stdout in parallel directory runs. This is an accepted trade-off for the initial parallel implementation — ScriptLoader parallel mode is only used for directory inputs where output ordering is less critical. A proper fix (refactoring `RunSource`/`ExecuteInterpreted` to support output capture and returning text via `AConsoleOutput`/`AErrorMessage`) has been deferred to a follow-up. Do not re-flag the direct `RunScriptFromFile` call in `ScriptWorkerProc` as a regression or issue until the output-capture refactor lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: units/Goccia.Coverage.pas:406-422
Timestamp: 2026-04-15T06:26:41.145Z
Learning: In frostney/GocciaScript, `GetJobCount` (in `units/Goccia.CLI.Application.pas`) intentionally returns 1 when `--coverage` is enabled, forcing sequential execution for coverage runs. Per-thread `TGocciaCoverageTracker` initialization and cross-thread coverage merging are deferred to a follow-up. Do not re-flag missing worker-thread coverage initialization in `TGocciaFileWorker.Execute` (`units/Goccia.Threading.pas`) — the fix is that parallel mode is disabled for coverage, not that workers initialize a tracker.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: TestRunner.dpr:237-240
Timestamp: 2026-04-07T20:24:01.235Z
Learning: Repo: frostney/GocciaScript — TestRunner.dpr / Goccia.Coverage
The synthetic `runTests({...})` call is appended to `Source` before lexing in both `RunGocciaScriptBytecode` and `RunGocciaScriptInterpreted`. `Lexer.SourceLines` therefore includes this line, and `CountExecutableLines` counts it as +1 executable. Because the engine always executes it, coverage also records +1 hit. The net effect on the percentage is self-consistent (+1/+1) and does NOT skew coverage ratios. `PrintCoverageDetail` reads the original file from disk, so the synthetic line never appears in the detail output. Do not flag this as a coverage accuracy bug.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 200
File: REPL.dpr:0-0
Timestamp: 2026-04-07T17:12:07.324Z
Learning: When reviewing timing/metrics code in the GocciaScript repo (e.g., REPL.dpr, ScriptLoader, TestRunner, BenchmarkRunner, and TGocciaEngine.Execute), follow the established timing convention: capture `StartTime` before the JSX pre-pass (`TGocciaJSXTransformer.Transform`) and `StringListToLFText`, then capture `LexEnd` after `ScanTokens`. The `Lex` duration (`LexEnd - StartTime`) is intended to include the JSX pre-pass because it is treated as part of the source→tokens pipeline. Do not flag this ordering as a bug and do not recommend moving `StartTime` to after the JSX transform, as it would break the bucket semantics.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 289
File: ScriptLoader.dpr:140-156
Timestamp: 2026-04-12T21:19:18.811Z
Learning: For GocciaScript CLI tools, suppress human-readable status/progress WriteLn output when running in JSON output mode. In ScriptLoader (ScriptLoader.dpr), gate any status/progress WriteLn lines on `not GJsonOutput`, because `--output=json` streams the result document directly to stdout (mixing status text would corrupt JSON). In TestRunner and BenchmarkRunner (TestRunner.dpr, BenchmarkRunner.dpr), JSON is written to a file, so current unconditional stdout WriteLn calls are safe, but these tools should be refactored later to follow the same stdout/JSON suppression pattern used by ScriptLoader.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: units/Goccia.Timeout.pas:26-29
Timestamp: 2026-04-15T06:27:18.195Z
Learning: In frostney/GocciaScript (PR `#306`, units/Goccia.Timeout.pas), `StartExecutionTimeout` / `ClearExecutionTimeout` / `CheckExecutionTimeout` use `threadvar` storage. `StartExecutionTimeout` is called inside execution functions (`RunGocciaScriptInterpreted`, `ExecuteInterpreted`, `CollectBenchmarkFileInterpreted`, etc.) that run directly on worker threads — NOT on the main thread. Each worker thread correctly initializes its own timeout state before the engine loop calls `CheckExecutionTimeout`. The CLI timeout option value is a read-only/immutable CLI option object shared safely across threads. Do NOT re-flag the `threadvar` timeout design as leaving worker threads un-initialized.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: ScriptLoader.dpr:724-737
Timestamp: 2026-04-14T06:46:14.795Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), `--emit` with `--output=<file>` and multiple input paths or a directory input silently overwrites the same output file for each compiled module. This is pre-existing behavior preserved across the CLI refactor in PR `#299` and should NOT be re-flagged as a regression. A follow-up guard (raising TGocciaParseError when FEmit.Present and FOutputPath.Present and APaths.Count > 1 or the single path is a directory) was proposed but deferred.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T06:46:10.919Z
Learning: In frostney/GocciaScript CLI tool sources, the duplicated `EscapeJSONString` helper intentionally escapes only `\`, `"`, `\n`, `\r`, and `\t` (leaving other C0 control characters U+0000–U+001F unescaped). During code review, do not flag missing additional control-character escaping in these per-tool `EscapeJSONString` copies while the shared-helper consolidation/fix PR is not yet landed; only re-check once the implementation has been consolidated into the shared helper and the tracking fix is in place.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: units/Goccia.Builtins.GlobalPromise.pas:56-57
Timestamp: 2026-04-15T06:26:30.835Z
Learning: In frostney/GocciaScript (PR `#306`), ~30 builtin units (e.g., Goccia.Builtins.GlobalPromise.pas, Goccia.Builtins.GlobalArray.pas, Goccia.Builtins.Math.pas, and ~27 others) use `threadvar FStaticMembers: TArray<TGocciaMemberDefinition>` in their `implementation` sections. FPC does NOT finalize managed threadvar contents on thread exit, so the TArray shell (~few hundred bytes per unit) leaks per worker thread. This is an accepted/deferred trade-off: (1) threads are short-lived spawn-and-join workers, (2) the GC-managed TGocciaMemberDefinition objects are cleaned up by ShutdownThreadRuntime, (3) OS reclaims all memory on process exit, (4) a proper fix requires a registration-based cleanup system or per-unit finalization hooks since the threadvars are in implementation sections. Do NOT re-flag this managed-threadvar leak until long-lived thread pools are introduced and a cleanup registry is in place.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T13:53:18.321Z
Learning: For GocciaScript CLI tool sources in this repo, when you see duplicated `EscapeJSONString` helpers that only escape `\`, `"`, `\n`, `\r`, and `\t` (leaving other C0 control characters U+0000–U+001F unescaped) in individual per-tool `.dpr` files, do not re-flag this as a review issue while the consolidation/fix tracked in issue `#303` has not landed. After the PR resolving `#303` is merged, start reviewing these helpers for full control-character JSON escaping again.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 286
File: docs/benchmarks.md:121-123
Timestamp: 2026-04-12T15:44:31.303Z
Learning: In frostney/GocciaScript BenchmarkRunner.dpr, both interpreted and bytecode execution paths use the same `[ggBenchmark]` globals set. `TGocciaEngine` is constructed with `[ggBenchmark]` for interpreted mode, and `TGocciaBytecodeBackend.RegisterBuiltIns([ggBenchmark])` is called for bytecode mode. Documentation referring to `[ggBenchmark]` as the globals for the benchmark runtime is accurate for both modes. Do not flag the doc step as mode-inaccurate on the basis of different backend types.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 286
File: docs/testing.md:774-776
Timestamp: 2026-04-12T15:44:39.321Z
Learning: In frostney/GocciaScript TestRunner.dpr, both interpreted (`RunGocciaScriptInterpreted` via `TGocciaEngine`) and bytecode (`RunGocciaScriptBytecode` via `TGocciaBytecodeBackend`) paths use the same globals set `[ggTestAssertions, ggFFI]`. `TGocciaBytecodeBackend.RegisterBuiltIns` accepts the same `TGocciaGlobalBuiltins` type. Documentation stating "creates a fresh `TGocciaEngine` with `[ggTestAssertions, ggFFI]`" is intentionally concise and accurate for both modes — do not flag it as incomplete for omitting the bytecode backend distinction.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: ScriptLoader.dpr:727-740
Timestamp: 2026-04-14T13:51:07.273Z
Learning: In frostney/GocciaScript (ScriptLoader.dpr), the `--emit` flag (bytecode compilation and output to a `.gbc` file) is an exploratory/temporary feature added to validate the bytecode pipeline. It is NOT the long-term solution for bytecode output. A proper bundling mechanism is being designed separately and tracked in issue `#156`. Do not flag `--emit` multi-file overwrite behavior or the presence of bytecode emission logic in ScriptLoader as architectural problems — the entire emit path is expected to be replaced by the bundler. ScriptLoader doing bytecode output is acknowledged as "too much responsibility" by the maintainer.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:211-212
Timestamp: 2026-03-23T12:37:30.936Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas (PR `#107`)
Learning: In TGocciaRuntimeOperations.MarkExternalRoots, entries in FModuleCache (cached module namespace/JSON records) must be traversed and their TSouffleValue references marked as external roots to survive GC. This behavior was added in commit 36f69cc; do not re‑flag missing FModuleCache rooting unless the logic regresses.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.pas : Built-ins are registered by the engine via `TGocciaGlobalBuiltins` flags. The TestRunner adds `ggTestAssertions` for the test framework. The BenchmarkRunner adds `ggBenchmark` for the benchmark framework.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 139
File: units/Goccia.ScriptLoader.JSON.Test.pas:1-113
Timestamp: 2026-03-30T13:02:33.857Z
Learning: In frostney/GocciaScript, CI workflows (`.github/workflows/ci.yml` and `.github/workflows/pr.yml`) use glob patterns (`build/Goccia.*.Test*` for artifact upload, `Goccia.*.Test` / `Goccia.*.Test.exe` for execution) to discover and run native Pascal test binaries automatically. Do not flag missing entries in a hard-coded test list — new `units/*.Test.pas` test programs are picked up automatically via these globs as of commit b63911d (PR `#139`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 60
File: units/Goccia.Runtime.Operations.pas:8694-8696
Timestamp: 2026-03-10T18:05:46.926Z
Learning: Repo: frostney/GocciaScript PR: 60 — units/Goccia.Runtime.Operations.pas: The GC rooting of FArrayBridgeReverse (marking values in MarkExternalRoots and keys in MarkWrappedGocciaValues) is intentional to preserve array identity across async/microtask boundaries (e.g., Promise.resolve(arr).then(v => v === arr)). Do not prune this map during normal clearing; per-file GC.Collect in runners bounds cross-file memory growth.

- Force sequential execution when --exit-on-first-failure is set
- Force sequential when --coverage is enabled (not yet thread-safe)
- Convert GRegExpPrototype to threadvar to prevent cross-thread races
- Convert GNextSymbolId/GSymbolRegistry to threadvar for thread isolation
- Guard FSharedPrototype assignment in FunctionBase with try/except
- Handle uninitialized result slots on fatal worker thread exceptions
- Fix CPU detection: use sysconf directly (FPC 3.2.2 bug workaround)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai coderabbitai Bot added the new feature New feature or request label Apr 15, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@units/Goccia.CLI.Application.pas`:
- Around line 258-260: GetJobCount currently only checks
FCoverageOptions.Enabled.Present before forcing sequential execution which
misses cases where InitializeCoverageIfEnabled turns on coverage via
Format.Present or OutputPath.Present; update the check in GetJobCount to also
consider FCoverageOptions.Format.Present and FCoverageOptions.OutputPath.Present
(or whatever properties InitializeCoverageIfEnabled reads) so that if any of
Enabled.Present, Format.Present, or OutputPath.Present is true you call Exit(1)
to force sequential mode; reference the GetJobCount routine and
FCoverageOptions.Enabled/Format/OutputPath fields to locate and modify the logic
to mirror InitializeCoverageIfEnabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 466bd623-3fe7-474d-a1dc-906af8e72f56

📥 Commits

Reviewing files that changed from the base of the PR and between 7916c07 and f121f5f.

📒 Files selected for processing (6)
  • TestRunner.dpr
  • units/Goccia.CLI.Application.pas
  • units/Goccia.RegExp.Runtime.pas
  • units/Goccia.Threading.pas
  • units/Goccia.Values.FunctionBase.pas
  • units/Goccia.Values.SymbolValue.pas
🚧 Files skipped from review as they are similar to previous changes (2)
  • units/Goccia.Values.FunctionBase.pas
  • units/Goccia.Values.SymbolValue.pas

Comment thread units/Goccia.CLI.Application.pas Outdated
Instead of forcing sequential execution when these flags are set,
implement proper parallel support:

- Add CancelOnError to TGocciaThreadPool: when a worker detects a
  failure, it sets the shared cancelled flag so all workers skip
  remaining files. Up to N in-flight files may complete (expected).

- Add EnableCoverage to TGocciaThreadPool: each worker initialises
  its own thread-local coverage tracker. After all workers complete,
  MergeCoverageInto collects per-thread coverage data into the main
  tracker via the new TGocciaCoverageTracker.MergeFrom method.

- Remove the GetJobCount hack that forced --jobs=1 for coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
units/Goccia.Coverage.pas (1)

515-518: Line hit merge loses actual hit counts — only records presence.

The comment (lines 81-83) says "Line hits and branch hits are summed," but RecordLineHit(I) increments by 1, not by SrcFile.GetLineHitCount(I). If a line was hit 5 times in a worker, the merged result shows 1 hit.

This is fine for coverage percentage calculations (LinesHit checks > 0), but if lcov/JSON export requires accurate hit counts, they'll be underreported in parallel mode.

♻️ Optional fix to preserve actual hit counts
     { Merge line hits — sum counts for each line. }
     for I := 1 to SrcFile.LineHitCount - 1 do
-      if SrcFile.GetLineHitCount(I) > 0 then
-        DstFile.RecordLineHit(I);
+    begin
+      HitCount := SrcFile.GetLineHitCount(I);
+      while HitCount > 0 do
+      begin
+        DstFile.RecordLineHit(I);
+        Dec(HitCount);
+      end;
+    end;

Or add a RecordLineHits(ALine, ACount: Integer) method for efficiency.

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

In `@units/Goccia.Coverage.pas` around lines 515 - 518, The merge loop currently
discards actual hit counts by calling DstFile.RecordLineHit(I) (which increments
by 1) inside the for I := 1 to SrcFile.LineHitCount - 1 loop; change it to add
the exact count returned by SrcFile.GetLineHitCount(I) to the destination.
Implement or use a method like DstFile.RecordLineHits(ALine: Integer; ACount:
Integer) and call DstFile.RecordLineHits(I, SrcFile.GetLineHitCount(I)) (or, if
you prefer not to add a new API, add the returned count in a small loop or an
existing accumulator) so merged results preserve the true hit counts rather than
only presence.
units/Goccia.CLI.Application.pas (1)

72-104: Document or prepare for non-glibc Linux and BSD support.

The hardcoded value SC_NPROCESSORS_ONLN = 84 is correct for glibc-based Linux, which covers all current CI targets (x86_64-linux, aarch64-linux). However, musl libc (Alpine Linux), FreeBSD, NetBSD, and OpenBSD define different numeric values for this constant. While not an issue today, document the glibc assumption in a comment or consider adding a FreeBSD case if future platforms are targeted, since the value 84 will not work for those systems.

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

In `@units/Goccia.CLI.Application.pas` around lines 72 - 104, GetProcessorCount
currently hardcodes SC_NPROCESSORS_ONLN = 84 for non-DARWIN UNIX which assumes
glibc; document this assumption and/or add platform-specific branches for musl
and BSDs by extending the conditional around SC_NPROCESSORS_ONLN (or adding new
constants) so libc_sysconf calls use the correct numeric constant on
FreeBSD/NetBSD/OpenBSD and musl-based Linux; update the comment above
libc_sysconf/GetProcessorCount to state the glibc assumption and, if you want
runtime support, add {$IFDEF FREEBSD}/{$IFDEF NETBSD}/{$IFDEF OPENBSD} cases (or
detect libc/machine at compile time) and set SC_NPROCESSORS_ONLN appropriately
so GetProcessorCount returns correct values across those platforms.
🤖 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.Threading.pas`:
- Around line 98-100: Remove the duplicated comment above the CancelOnError
property: keep a single descriptive comment for the property that references
FCancelOnError and CancelOnError, and delete the redundant duplicate line so
only one "{ When True, the first worker error automatically cancels remaining
files. }" remains immediately above "property CancelOnError: Boolean read
FCancelOnError write FCancelOnError;".

---

Nitpick comments:
In `@units/Goccia.CLI.Application.pas`:
- Around line 72-104: GetProcessorCount currently hardcodes SC_NPROCESSORS_ONLN
= 84 for non-DARWIN UNIX which assumes glibc; document this assumption and/or
add platform-specific branches for musl and BSDs by extending the conditional
around SC_NPROCESSORS_ONLN (or adding new constants) so libc_sysconf calls use
the correct numeric constant on FreeBSD/NetBSD/OpenBSD and musl-based Linux;
update the comment above libc_sysconf/GetProcessorCount to state the glibc
assumption and, if you want runtime support, add {$IFDEF FREEBSD}/{$IFDEF
NETBSD}/{$IFDEF OPENBSD} cases (or detect libc/machine at compile time) and set
SC_NPROCESSORS_ONLN appropriately so GetProcessorCount returns correct values
across those platforms.

In `@units/Goccia.Coverage.pas`:
- Around line 515-518: The merge loop currently discards actual hit counts by
calling DstFile.RecordLineHit(I) (which increments by 1) inside the for I := 1
to SrcFile.LineHitCount - 1 loop; change it to add the exact count returned by
SrcFile.GetLineHitCount(I) to the destination. Implement or use a method like
DstFile.RecordLineHits(ALine: Integer; ACount: Integer) and call
DstFile.RecordLineHits(I, SrcFile.GetLineHitCount(I)) (or, if you prefer not to
add a new API, add the returned count in a small loop or an existing
accumulator) so merged results preserve the true hit counts rather than only
presence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea2dc9f3-c80f-4c4d-bbae-5ea846d901ef

📥 Commits

Reviewing files that changed from the base of the PR and between f121f5f and 3bf539e.

📒 Files selected for processing (4)
  • TestRunner.dpr
  • units/Goccia.CLI.Application.pas
  • units/Goccia.Coverage.pas
  • units/Goccia.Threading.pas
✅ Files skipped from review due to trivial changes (1)
  • TestRunner.dpr

Comment thread units/Goccia.Threading.pas
Round-robin pre-partitioned files across workers at startup, causing
heavy imbalance when file execution times vary (e.g. 183s vs 19s in
benchmarks). Workers with short files finished early while one worker
was stuck with all the heavy files.

Replace with a shared TGocciaWorkQueue protected by a critical section.
Each worker pulls the next file when ready, so fast workers naturally
pick up more files and all cores stay busy.

Results on full benchmark suite (21 files, 10 workers):
  Round-robin: 8m 03s → Work-stealing: 5m 01s (4.0× vs sequential)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@units/Goccia.Threading.pas`:
- Around line 327-343: The current startup loop can leak a freed Queue if a
TGocciaFileWorker.Create/Start raises; fix by tracking how many workers
successfully started (use FWorkers and a StartedCount), and on any exception set
FCancelled := True to signal workers to stop, then iterate the started workers
and call WaitFor (and Free) on each to ensure they exit before allowing
Queue.Free to run; wrap the worker-creation loop in try/except to perform this
cleanup and re-raise the exception, so Queue (TGocciaWorkQueue) is only freed
after all started TGocciaFileWorker instances have been waited on and freed.
- Around line 305-317: Reset the pool state at the top of
TGocciaThreadPool.RunAll by clearing the cancelled flag so previous
Cancel()/CancelOnError runs don't taint subsequent calls: inside RunAll (e.g.,
immediately after begin) set FCancelled := False (and any other per-run
flags/results if present) before proceeding with FileCount handling so the pool
will execute the new batch normally.
- Around line 72-73: FCancelled is accessed concurrently causing a data race;
synchronize reads/writes by guarding all accesses to FCancelled with the
existing queue critical section (the same
EnterCriticalSection/LeaveCriticalSection pattern used in TGocciaWorkQueue) or
replace FCancelled with an atomic/interlocked flag and use
InterlockedExchange/CompareExchange for writes and a single atomic read for
checks. Update the worker loop that reads FCancelled, the error-path that sets
FCancelled, and the Cancel() method to use the chosen synchronization (reference
symbols: FCancelled, Cancel, the queue lock methods/critical section used by
TGocciaWorkQueue) so no unsynchronized reads/writes remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eed70509-1fff-44c8-851c-cabfd53f678b

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf539e and b53e0d7.

📒 Files selected for processing (1)
  • units/Goccia.Threading.pas

Comment thread units/Goccia.Threading.pas
Comment thread units/Goccia.Threading.pas
Comment thread units/Goccia.Threading.pas
frostney and others added 2 commits April 15, 2026 09:16
- Add Goccia.Threading.Test.pas with 10 unit tests covering:
  work queue drain/order/empty, pool file dispatch, result ordering,
  cancel, cancel-on-error, state reset between runs, empty list,
  and single-worker mode.

- Reset FCancelled at start of RunAll so a pool can be reused after
  a previous Cancel or CancelOnError run.

- Pre-nil FWorkers array and move WaitFor into the finally block so
  a Create/Start failure doesn't free the queue while workers are
  still running.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three module-level variables in the bytecode compiler were shared
across all worker threads, causing intermittent crashes (Bus error,
Access violation, Invalid pointer operation) and corrupted bytecode
output (ReferenceError: true/false is not defined) during parallel
execution:

- GBreakJumps, GPendingFinally, GBreakFinallyBase in
  Goccia.Compiler.Statements.pas — break/finally jump tracking
  corrupted when two workers compiled simultaneously

- GUsingResources in Goccia.Compiler.Statements.pas — using/dispose
  resource tracking shared across threads

- GClassPrivateCounter in Goccia.Compiler.Scope.pas — private field
  prefix counter incremented without synchronisation

All converted to threadvar. Verified with 10 consecutive parallel
bytecode runs: 0 crashes, 0 intermittent failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@frostney frostney merged commit 69b6c37 into main Apr 15, 2026
10 checks passed
@frostney frostney deleted the t3code/multi-file-runner-concurrency branch April 15, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Refactoring, CI, tooling, cleanup new feature New feature or request performance Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant