Skip to content

Add compat var declarations#368

Merged
frostney merged 14 commits intomainfrom
t3code/8d1b55e1
Apr 20, 2026
Merged

Add compat var declarations#368
frostney merged 14 commits intomainfrom
t3code/8d1b55e1

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Apr 20, 2026

Summary

  • Adds --compat-var / CompatVar support to enable var declarations through the CLI, engine, interpreter, parser, compiler, bundler, REPL, loader, benchmark runner, and test runner.
  • Implements var hoisting and redeclaration semantics in both interpreter and compiler paths, including destructuring support and function/module-scope binding behavior.
  • Adds language tests covering var declaration behavior, scoping, hoisting, closures, redeclarations, and destructuring.

- Add `compat-var` flag across CLI, engine, parser, interpreter, and compiler
- Support function-scoped `var` hoisting, redeclaration, and destructuring
- Add language tests for `var` behavior
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 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: f9f9206c-c2ab-45c3-aa7b-95fedada4fcd

📥 Commits

Reviewing files that changed from the base of the PR and between d6dddf6 and 568bc71.

📒 Files selected for processing (1)
  • source/units/Goccia.Compiler.Statements.pas
🚧 Files skipped from review as they are similar to previous changes (1)
  • source/units/Goccia.Compiler.Statements.pas

📝 Walkthrough

Walkthrough

Adds opt-in ECMAScript var support via a new --compat-var flag (cfVar); threads the flag through CLI, engine, parser, interpreter, compiler, VM, module loader and apps to implement parsing, hoisting, function/module-scoped var bindings, destructuring, redeclaration, and tests/docs.

Changes

Cohort / File(s) Summary
CLI & Engine
source/shared/CLI.Options.pas, source/units/Goccia.Engine.pas, source/app/Goccia.CLI.Application.pas
Add CompatVar CLI flag and cfVar compatibility bit; expose TGocciaEngine.VarEnabled and propagate during initialization.
Apps & Tooling
source/app/GocciaBundler.dpr, source/app/GocciaBenchmarkRunner.dpr, source/app/GocciaREPL.dpr, source/app/GocciaTestRunner.dpr, source/app/GocciaScriptLoader.dpr
Thread compat-var into engine/parser setup for CLI apps; TestRunner adds per-file config detection and applies Engine.VarEnabled.
Parser
source/units/Goccia.Parser.pas
Add VarDeclarationsEnabled property; parse var statements (including destructuring), accept var in for(... of ...), thread flag into interpolation/template parsing.
AST / Statements
source/units/Goccia.AST.Statements.pas
Add IsVar flag to variable/destructuring declarations and for...of nodes; extend constructors and expose properties.
Compiler / Hoisting
source/units/Goccia.Compiler.pas, source/units/Goccia.Compiler.Statements.pas, source/units/Goccia.Compiler.Scope.pas
Introduce DeclareVarLocal, hoisting helpers (HoistVarLocals), collect destructuring-var bindings, and compile var with depth-0 locals, redeclaration handling, and suppressed global-backing for var where appropriate.
Interpreter / Evaluator / Runtime
source/units/Goccia.Interpreter.pas, source/units/Goccia.Evaluator.pas, source/units/Goccia.Values.FunctionValue.pas, source/units/Goccia.Modules.Loader.pas
Add VarEnabled flags; hoist var names into function/module/global scopes at runtime; change for-of/destructuring evaluation to define/update var bindings on function/module scope; propagate flag to module loader.
Scope & Binding Map
source/units/Goccia.Scope.pas, source/units/Goccia.Scope.BindingMap.pas
Add dtVar and per-scope FVarBindings; add DefineVariableBinding, FindFunctionOrModuleScope; rename AssignLexicalBindingAssignBinding/GetBinding; include var bindings in lookup, assignment, GC marking, and contains logic.
VM & Assignment Sites
source/units/Goccia.VM.pas, source/units/Goccia.AST.Expressions.pas
Switch lexical-only assignment calls to generic AssignBinding to respect var vs lexical semantics in VM and expression evaluators.
Module / Parser plumbing
source/units/Goccia.Modules.Loader.pas, source/app/...
Ensure parser/module-loader instances receive VarDeclarationsEnabled/VarEnabled consistently where engines/parsers are created.
Tests & Docs
tests/language/var/*, tests/language/var/goccia.json, docs/*
Add comprehensive compat-var test suite and update docs/decision log and language docs to document opt-in var behavior and implementation notes.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Config
    participant Engine as Engine
    participant Parser as Parser
    participant Compiler as Compiler
    participant Interpreter as Interpreter
    participant ModuleLoader as ModuleLoader

    CLI->>Engine: set CompatVar (--compat-var)
    Engine->>Parser: VarDeclarationsEnabled = CompatVar
    Engine->>ModuleLoader: VarEnabled = CompatVar
    alt compile path
        Parser->>Compiler: AST (nodes with IsVar)
        Compiler->>Compiler: HoistVarLocals into function/module scope
        Compiler->>VM: emit bytecode using var depth-0 slots / reuse
    else interpret path
        Parser->>Interpreter: AST (nodes with IsVar)
        Interpreter->>Interpreter: HoistVarDeclarations into call/module scope
        Interpreter->>Evaluator: execute with function-scoped var semantics
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add compat var declarations' clearly and concisely describes the main change: adding opt-in var declaration support through the --compat-var compatibility flag.
Description check ✅ Passed The PR description comprehensively addresses the template requirements: summarizes the change (opt-in var support across multiple components), documents implementation scope (hoisting, redeclaration, destructuring, scoping), and references related testing coverage.
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 new feature New feature or request spec compliance Mismatch against official JavaScript/TypeScript specification labels Apr 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

Benchmark Results

386 benchmarks

Interpreted: 🟢 89 improved · 🔴 181 regressed · 116 unchanged · avg -3.8%
Bytecode: 🟢 326 improved · 🔴 15 regressed · 45 unchanged · avg +9.0%

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

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 20, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 7518 7518
Tests Passed 7477 ✅ 7518 ✅
Tests Skipped 41 0
Tests Workers 4 4
Tests Test Duration 1.50s 1.40s
Tests Lex (cumulative) 180.0ms 109.6ms
Tests Parse (cumulative) 270.5ms 263.8ms
Tests Compile (cumulative) 170.4ms
Tests Execute (cumulative) 1.78s 1.42s
Tests Engine Total (cumulative) 2.23s 1.96s
Tests Lex (avg/worker) 45.0ms 27.4ms
Tests Parse (avg/worker) 67.6ms 65.9ms
Tests Compile (avg/worker) 42.6ms
Tests Execute (avg/worker) 444.2ms 354.9ms
Tests Engine Total (avg/worker) 556.9ms 490.9ms
Benchmarks Total 386 386
Benchmarks Workers 4 4
Benchmarks Duration 3.11min 2.89min

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

🧹 Nitpick comments (1)
tests/language/var/closure-over-var.js (1)

6-24: Make one case escape the defining function.

Both tests call getter() before fn() returns, so they never verify that the captured var environment survives after the outer activation is gone. A broken implementation that tears down the environment eagerly can still pass here. Consider returning the closure, mutating first, and asserting after the outer function returns.

Based on learnings Applies to tests/**/*.{js,jsx,ts,tsx,mjs} : JavaScript tests should cover happy paths, edge cases, and error cases. Keep tests isolated and grouped by feature/filename.

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

In `@tests/language/var/closure-over-var.js` around lines 6 - 24, Change one of
the tests so the closure escapes the defining function: update either the
"closure captures var from enclosing function" or "var assignment visible to
previously-created closure" test so that fn returns the closure (getter) instead
of invoking it inside fn, perform the mutation to the var (x or value) before
returning the closure, then call the returned closure from the test scope and
assert it sees the updated value; reference the functions and vars by name (fn,
getter, x/value) so the change is applied to the correct test case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/units/Goccia.Compiler.pas`:
- Around line 307-358: Hoisting currently only walks a subset of container
nodes, so HoistVarLocals misses var bindings from destructuring, export
declarations, and for-await-of bodies; update HoistVarLocals to also handle
TGocciaForAwaitOfStatement (visit its Body), TGocciaExportNamedDeclaration
(visit its Declaration), and any node types that can carry
TGocciaVariableDeclaration children (e.g., inspect
TGocciaVariableDeclarator/destructuring inside declarations) so that whenever a
TGocciaVariableDeclaration or a node wrapping one is encountered you call
AScope.DeclareVarLocal for each Variables[i].Name; modify the existing branches
in HoistVarLocals to include these node types and/or add a generic traversal
that examines child declaration fields to ensure all var declarators (including
destructured and exported ones) are visited.

In `@source/units/Goccia.Compiler.Scope.pas`:
- Around line 157-165: The new local is being initialized with Depth := 0 which
breaks scope unwinding in nested scopes; update the local's Depth to the current
scope depth instead of 0 (use the variable that tracks current scope depth in
this unit — e.g., FCurrentDepth or FScopeDepth) when assigning
FLocals[FLocalCount].Depth so EndScope can correctly pop locals; keep the
FNextSlot overflow check and resizing logic for FLocals and ensure EndScope
still matches against that depth when unwinding captured/non-captured locals.

In `@source/units/Goccia.Compiler.Statements.pas`:
- Around line 476-479: In CompileVariableDeclaration, the no-initializer branch
currently emits OP_LOAD_UNDEFINED unconditionally and thus overwrites an
existing value when AStmt.IsVar uses ACtx.Scope.DeclareVarLocal to reuse a slot
for a redeclared var; modify the logic so after calling
ACtx.Scope.DeclareVarLocal(Info.Name) you detect whether it returned an existing
slot (i.e., a redeclaration without new storage) and skip emitting
OP_LOAD_UNDEFINED in that case; reference the functions/methods
CompileVariableDeclaration, AStmt.IsVar, ACtx.Scope.DeclareVarLocal and the
OP_LOAD_UNDEFINED emission point, and add a unit test that covers the redeclared
var-without-initializer case (e.g., var x = 1; var x;) to ensure the previous
value is preserved.

In `@source/units/Goccia.Engine.pas`:
- Around line 1467-1474: TGocciaEngine.SetVarEnabled correctly flips the cfVar
flag and sets FInterpreter.VarEnabled but the module execution path in
TGocciaInterpreterExecutor.EvaluateModuleBody still skips a `var` hoist pass;
update EvaluateModuleBody to run the same hoisting logic used for scripts when
`cfVar`/FInterpreter.VarEnabled is true (i.e., perform a var-declaration
collection/hoist before evaluating module statements) so imported modules get
proper `var` hoisting semantics; reference the compatibility flag
(FCompatibility/cfVar), the engine setter (TGocciaEngine.SetVarEnabled) and the
executor method (TGocciaInterpreterExecutor.EvaluateModuleBody) when adding the
hoist pass and ensure the hoist respects existing interpreter state
(FInterpreter.VarEnabled).

In `@source/units/Goccia.Evaluator.pas`:
- Around line 161-211: CollectVarNamesFromNode currently only adds simple
TGocciaVariableDeclaration variable names and recurses TGocciaForOfStatement
bodies, so destructured declarations and for-await-of bodies are skipped; update
CollectVarNamesFromNode to (1) handle destructuring by iterating each
TGocciaVariableDeclaration.Variables (TGocciaVariableDeclarator) and when a
declarator has no simple Name, walk its pattern nodes (e.g.,
TGocciaObjectPattern, TGocciaArrayPattern and any nested Property/Element
patterns) to extract and ANames.Add each identifier found, and (2) add a branch
for TGocciaForAwaitOfStatement (like TGocciaForOfStatement) to recurse into its
Body so var declarations inside for-await-of are collected. Ensure you use the
existing CollectVarNamesFromNode recursion for nested pattern elements and
reference TGocciaVariableDeclaration, TGocciaVariableDeclarator,
TGocciaObjectPattern, TGocciaArrayPattern, and TGocciaForAwaitOfStatement when
implementing.

In `@source/units/Goccia.Interpreter.pas`:
- Around line 138-139: The interpreter currently only hoists var declarations
for the main program body (HoistVarDeclarations(AProgram.Body, FGlobalScope)),
so standalone modules loaded via TGocciaModuleLoader.LoadModule (which creates
an skModule child scope) never get var hoisting; update
TGocciaInterpreter.EvaluateModuleBody to, when FVarEnabled, call
HoistVarDeclarations on the module's statement list using the module's execution
scope (the skModule scope created by TGocciaModuleLoader.LoadModule) instead of
always using FGlobalScope so that var reads-before-declaration in imported
modules are initialized to undefined.

In `@source/units/Goccia.Parser.pas`:
- Around line 2869-2873: The parser branch that handles const/let/var currently
only sets IsConst and then Advance, so the loop binding loses whether it was a
var; modify the branch in Goccia.Parser.pas to capture the actual binding kind
(e.g., introduce an IsVar or BindingKind variable) by checking Check(gttVar)
(and FVarDeclarationsEnabled) as well as gttLet/gttConst, set the appropriate
flag before calling Advance, and ensure that the resulting AST node for the
for...of loop records that flag (instead of only IsConst) so downstream code can
distinguish var vs let/const semantics.
- Around line 195-196: ParseInterpolationExpression creates a new TGocciaParser
with default flags so nested template interpolations ignore the outer parser's
VarDeclarationsEnabled; update ParseInterpolationExpression so after
constructing the nested TGocciaParser instance (the one used to parse the
interpolation) you assign its VarDeclarationsEnabled :=
Self.VarDeclarationsEnabled (or otherwise propagate the outer parser's
FVarDeclarationsEnabled) so the nested parser honors the outer parser's
compat-var setting.
- Around line 2777-2784: The recovery for unsupported "var" uses
SkipUntilSemicolon which ignores ASI and block boundaries and can consume
closing braces (affecting Depth); replace the current SkipUntilSemicolon call in
the FVarDeclarationsEnabled fallback so it uses an ASI-aware recovery routine
(or implement one) that stops on an explicit ';', on an automatic-semicolon
insertion point (newline before a closing token/EOF), or before a '}' token so
Depth is not decremented; update the path that issues AddWarning and returns
TGocciaEmptyStatement to invoke that ASI-aware skipper instead of
SkipUntilSemicolon.

In `@tests/language/var/hoisting.js`:
- Around line 26-30: The test currently checks hoisting inside the test(...)
callback (function scope) rather than module scope; move the pre-declaration
read and the var declaration to file (module) scope so the read happens before
the var statement at module level, then inside the existing test callback assert
that the pre-read value is undefined and the post-declaration value equals
"assigned"; update references to hoistedVar and any local variables accordingly
(look for the test function named "var hoisted at module top level" and the
identifier hoistedVar).

---

Nitpick comments:
In `@tests/language/var/closure-over-var.js`:
- Around line 6-24: Change one of the tests so the closure escapes the defining
function: update either the "closure captures var from enclosing function" or
"var assignment visible to previously-created closure" test so that fn returns
the closure (getter) instead of invoking it inside fn, perform the mutation to
the var (x or value) before returning the closure, then call the returned
closure from the test scope and assert it sees the updated value; reference the
functions and vars by name (fn, getter, x/value) so the change is applied to the
correct test case.
🪄 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: 9ab09815-7c8f-40f4-9e92-c9e995a2b41a

📥 Commits

Reviewing files that changed from the base of the PR and between cdb1310 and ca316e6.

📒 Files selected for processing (26)
  • source/app/Goccia.CLI.Application.pas
  • source/app/GocciaBenchmarkRunner.dpr
  • source/app/GocciaBundler.dpr
  • source/app/GocciaREPL.dpr
  • source/app/GocciaScriptLoader.dpr
  • source/app/GocciaTestRunner.dpr
  • source/shared/CLI.Options.pas
  • source/units/Goccia.AST.Statements.pas
  • source/units/Goccia.Compiler.Scope.pas
  • source/units/Goccia.Compiler.Statements.pas
  • source/units/Goccia.Compiler.pas
  • source/units/Goccia.Engine.pas
  • source/units/Goccia.Evaluator.pas
  • source/units/Goccia.Interpreter.pas
  • source/units/Goccia.Modules.Loader.pas
  • source/units/Goccia.Parser.pas
  • source/units/Goccia.Scope.pas
  • source/units/Goccia.Values.FunctionValue.pas
  • tests/language/var/basic-declaration.js
  • tests/language/var/closure-over-var.js
  • tests/language/var/destructuring-var.js
  • tests/language/var/function-scoping.js
  • tests/language/var/goccia.json
  • tests/language/var/hoisting.js
  • tests/language/var/multiple-declarations.js
  • tests/language/var/redeclaration.js

Comment thread source/units/Goccia.Compiler.pas
Comment thread source/units/Goccia.Compiler.Scope.pas
Comment thread source/units/Goccia.Compiler.Statements.pas
Comment thread source/units/Goccia.Engine.pas
Comment thread source/units/Goccia.Evaluator.pas
Comment thread source/units/Goccia.Interpreter.pas
Comment thread source/units/Goccia.Parser.pas
Comment on lines +2777 to +2784
if not FVarDeclarationsEnabled then
begin
AddWarning('''var'' declarations are not supported in GocciaScript',
'Use ''let'' or ''const'' instead',
Line, Column);
SkipUntilSemicolon;
Result := TGocciaEmptyStatement.Create(Line, Column);
Exit;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 20, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsupported-var recovery breaks on ASI/block boundaries.

SkipUntilSemicolon() only stops on an explicit ;. In this fallback path, var x followed by a newline and } will consume the closing brace, drive Depth negative, and can skip the rest of the file instead of producing one warned no-op. Please use an ASI-aware recovery path here.

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

In `@source/units/Goccia.Parser.pas` around lines 2777 - 2784, The recovery for
unsupported "var" uses SkipUntilSemicolon which ignores ASI and block boundaries
and can consume closing braces (affecting Depth); replace the current
SkipUntilSemicolon call in the FVarDeclarationsEnabled fallback so it uses an
ASI-aware recovery routine (or implement one) that stops on an explicit ';', on
an automatic-semicolon insertion point (newline before a closing token/EOF), or
before a '}' token so Depth is not decremented; update the path that issues
AddWarning and returns TGocciaEmptyStatement to invoke that ASI-aware skipper
instead of SkipUntilSemicolon.

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.

Acknowledged. The SkipUntilSemicolon recovery path is only hit when --compat-var is disabled (the "var not supported" warning path). In that mode, ASI is also typically disabled, so the semicolon-based skip is sufficient for the common case. Will track as a follow-up improvement for edge cases where both ASI and unsupported-var recovery interact.

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 — that reasoning holds. Since the SkipUntilSemicolon fallback is only reachable when --compat-var is explicitly off, the user has already opted out of var support, and in practice they're almost certainly not running with --asi either. The edge case (ASI on, --compat-var off, var statement with no explicit ;) is a very narrow combination.

Would you like me to open a follow-up GitHub issue to track the ASI-aware recovery improvement so it doesn't get lost?


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 368
File: source/units/Goccia.Parser.pas:2780-2787
Timestamp: 2026-04-20T16:59:47.231Z
Learning: In `source/units/Goccia.Parser.pas`, the `SkipUntilSemicolon` call in `TGocciaParser.VarStatement`'s unsupported-`var` warning path (when `FVarDeclarationsEnabled = False`) is intentionally not ASI-aware. This path is only reachable when `--compat-var` is disabled, and ASI is also typically disabled in that mode, making the semicolon-based skip sufficient for the common case. The edge-case interaction (ASI enabled + `--compat-var` disabled) is acknowledged as a known gap and deferred to a follow-up improvement. Do NOT re-flag `SkipUntilSemicolon` in this specific fallback path until that follow-up is addressed.

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: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T15:16:53.061Z
Learning: Applies to **/Goccia.Parser.pas : The parser must accept unsupported syntax but treat it as a no-op — `function` declarations produce `TGocciaEmptyStatement`, `function` expressions evaluate to `undefined`, `==`/`!=` expressions evaluate to `undefined`, labeled statements strip the label and execute the labeled statement normally. A warning must be emitted to stdout with a suggestion

Learnt from: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Parser.pas:949-961
Timestamp: 2026-04-11T09:44:42.181Z
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: 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: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-05T07:58:33.798Z
Learning: Applies to **/{Parser,AST}*.pas : type/interface declarations and import type/export type must produce TGocciaEmptyStatement (no-op at runtime)

Learnt from: frostney
Repo: frostney/GocciaScript PR: 278
File: units/Goccia.Parser.pas:1010-1023
Timestamp: 2026-04-12T09:44:50.402Z
Learning: In frostney/GocciaScript (units/Goccia.Parser.pas, SplitTemplateAtBoundaries and the original ParseTaggedTemplate), the brace-counting used to find the closing `}` of a template interpolation expression (`${...}`) is NOT lexically aware — it does not account for `{`/`}` inside string literals, comments, or nested templates within the expression. This is a known pre-existing limitation that predates this codebase area; it was present in the original `ParseTaggedTemplate` on `main` and is carried forward. Fixing it properly requires lexer-time segmentation (embedding boundary markers during `ScanTemplate` while escape/string context is available). Do NOT re-flag the naive brace counter in SplitTemplateAtBoundaries or ParseTaggedTemplate as a regression or new bug — it is intentionally deferred to a follow-up. The limitation exists because `ScanTemplate` stores the entire template (including `${...}` expressions) as a single flat token with no expression-level lexical analysis.

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 : Do not add support for these JavaScript features: `var` declarations, `function` keyword, `==` and `!=` loose equality, `eval()` and `arguments` object, automatic semicolon insertion, `with` statement, traditional loops (`for`, `while`, `do...while`), default imports/exports, or global `parseInt`, `parseFloat`, `isNaN`, `isFinite`. The parser may accept some unsupported syntax as no-ops with warnings, but these features must not be implemented.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 53
File: souffle/Souffle.Wasm.Emitter.pas:0-0
Timestamp: 2026-03-08T04:50:29.540Z
Learning: In format.pas (GocciaScript formatter), ParseParamNames previously misidentified parenthesized arguments inside one-liner function bodies as parameter names, causing the `A` prefix to be prepended to them. The fix (f5b2f23) makes ParseParamNames ignore parentheses appearing after the first semicolon. Be cautious when reviewing one-liner procedure/function bodies in .pas files for incorrectly prefixed identifiers — this was the root cause of the AGC_ANY_CONVERT_EXTERN → AGC_ANY_CONVERT_EXTERN typos in souffle/Souffle.Wasm.Emitter.pas.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 306
File: units/Goccia.Builtins.GlobalPromise.pas:56-57
Timestamp: 2026-04-15T06:26:35.760Z
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: 290
File: units/Goccia.Compiler.Statements.pas:613-649
Timestamp: 2026-04-13T00:22:03.392Z
Learning: In frostney/GocciaScript (units/Goccia.Compiler.Statements.pas), `CompileUsingDeclaration` intentionally does not apply `Info.TypeAnnotation`, strict-type hints, or emit `OP_CHECK_TYPE` for `using`/`await using` bindings. This is a known, deferred gap — runtime validation via `OP_USING_INIT` covers correctness. The typed-local pipeline for `using` declarations will be wired up in a follow-up PR. Do NOT re-flag this gap in `CompileUsingDeclaration` until that follow-up lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 302
File: docs/language.md:118-118
Timestamp: 2026-04-14T13:18:31.918Z
Learning: In frostney/GocciaScript (units/Goccia.Lexer.pas, ScanTemplate / ProcessEscapeSequence), cooked=undefined support for tagged templates with malformed escape sequences (e.g., \u{, \xG1) landed in PR `#287` (TC39 Template Literal Revision / ES2018). Tagged templates now correctly set the cooked segment to undefined for malformed escapes while preserving the raw source text; untagged templates still throw SyntaxError. Do NOT re-flag this as missing or deferred.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 308
File: units/Goccia.VM.pas:3436-3437
Timestamp: 2026-04-15T15:41:51.609Z
Learning: Repo frostney/GocciaScript — units/Goccia.VM.pas:
For TGocciaVM.HasPropertyValue’s “in” operator error path (primitives on the RHS), the project intentionally uses SSuggestCheckNullBeforeAccess for all primitive cases (undefined, null, boolean, number, string). Branching the suggestion by value kind is deferred as low priority; do not re-flag this as an issue unless a dedicated follow-up changes that policy.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T06:46:13.250Z
Learning: In frostney/GocciaScript, `EscapeJSONString` (escaping `\`, `"`, `\n`, `\r`, `\t` only) is intentionally duplicated across ~5 CLI tool files (e.g., TestRunner.dpr, BenchmarkRunner.dpr). The incomplete control-character coverage (missing U+0000–U+001F beyond the five named characters) is a known pre-existing issue tracked for fix as part of a future consolidation into a shared helper. Do NOT re-flag missing control-character escaping in individual per-tool copies of `EscapeJSONString` until that consolidation PR lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 300
File: units/Goccia.VM.pas:0-0
Timestamp: 2026-04-14T06:31:04.274Z
Learning: In frostney/GocciaScript (units/Goccia.VM.pas), the OP_CHECK_TYPE handler intentionally calls `VMStrictTypeCheckRegisterValue(GetRegister(A), TGocciaLocalType(B))`, which boxes the register via `GetRegister(A)` before the type guard runs. A register-native fast path (passing `TGocciaRegister` directly and only materializing `TGocciaValue` for error text) is acknowledged as a potential optimization but is explicitly deferred to a future follow-up PR. Do NOT re-flag the `GetRegister(A)` boxing in the OP_CHECK_TYPE handler as a performance issue until that follow-up is scoped.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 222
File: scripts/test262_syntax_filter.py:430-434
Timestamp: 2026-04-08T14:59:30.654Z
Learning: In frostney/GocciaScript, `scripts/test262_syntax_filter.py` SKIP_PATH_SEGMENTS: the `for-in`, `eval-code`, and `generators` path segments are intentional categorical exclusions — every test under those directories requires a feature GocciaScript excludes by design (`for...in` loops, `eval()`, and generators respectively). Do not flag these as incorrect skips. Only the `asi/` segment may warrant conditional handling based on the `--asi` runner flag.

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 annotations in GocciaScript (TC39 Types as Comments proposal) are parsed but ignored at runtime. Raw type strings are preserved on AST nodes for potential future optimization. Use parser helpers `CollectTypeAnnotation(Terminators)` and `CollectGenericParameters` for type handling.

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: 201
File: units/Goccia.Coverage.pas:225-249
Timestamp: 2026-04-07T20:24:40.351Z
Learning: In `units/Goccia.Coverage.pas` `BuildExecutableLineFlags`: `interface Foo {` opening lines are intentionally marked as executable (`AFlags[I] := True`) because they produce `TGocciaEmptyStatement` which the engine does evaluate; interface body lines are suppressed via `TypeBodyDepth`. Single-line `type` aliases (e.g. `type Point = { x: number };`) are also correctly counted as executable for the same reason. The only known gap is multi-line `type` aliases spanning several lines — those continuation lines are not currently suppressed, but this is considered rare in practice and acceptable for now. Do not flag the `interface` opening-line executable marking or single-line `type` handling as bugs.

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 : Use `Goccia.Keywords.Reserved` and `Goccia.Keywords.Contextual` constants (`KEYWORD_THIS`, `KEYWORD_SUPER`, `KEYWORD_GET`, `KEYWORD_TYPE`, etc.) instead of hardcoded string literals when referencing JavaScript keywords.

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.Keywords.Reserved` and `Goccia.Keywords.Contextual` constants instead of hardcoded string literals when referencing JavaScript keywords (e.g., `KEYWORD_THIS`, `KEYWORD_SUPER`, `KEYWORD_GET`).

Learnt from: frostney
Repo: frostney/GocciaScript PR: 343
File: source/units/Goccia.Evaluator.Arithmetic.pas:49-55
Timestamp: 2026-04-18T21:08:03.166Z
Learning: In frostney/GocciaScript (commit 3344dede, PR `#343`), `TGocciaBigIntObjectValue` was added to support BigInt boxing via `Object(1n)`. `Box` in `ClassHelper.pas` now returns a `TGocciaBigIntObjectValue` for `TGocciaBigIntValue` primitives. `ToPrimitive` calls were added to `EvaluateSubtraction`, `EvaluateMultiplication`, `EvaluateDivision`, `EvaluateModulo`, and `EvaluateExponentiation` in `source/units/Goccia.Evaluator.Arithmetic.pas` so that boxed BigInts (e.g. `Object(1n) - 1n`) are correctly unboxed via `valueOf()` before BigInt type checks. `Object()` was also fixed to call `Box` for primitive arguments per ES2026 §20.1.1.1. Future reviews of arithmetic operators SHOULD expect `ToPrimitive` before BigInt checks.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 276
File: tests/built-ins/Goccia/build.js:6-7
Timestamp: 2026-04-11T18:29:34.210Z
Learning: In frostney/GocciaScript, the pattern `const hasGoccia = typeof Goccia !== "undefined"` combined with `describe.runIf(hasGoccia)(...)` is intentional and used across the entire `tests/` suite. It allows test files to run safely in non-GocciaScript environments (e.g., Vitest compatibility checks) where GocciaScript-specific globals like `Goccia` are not present. Do NOT flag this conditional-skip guard as a regression risk or suggest making suites unconditional — the portability requirement is by design.

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: 323
File: docs/language-tables.md:0-0
Timestamp: 2026-04-17T13:02:51.268Z
Learning: In frostney/GocciaScript, `atob`/`btoa` and `structuredClone` are NOT ECMAScript features — they belong to the WHATWG HTML spec. Since PR `#323` (commit 933ac45d), `docs/language-tables.md` has a dedicated **Web Platform APIs** section listing all 10 WHATWG/W3C APIs (`console`, `structuredClone`, `DOMException`, `atob`/`btoa`, `queueMicrotask`, `URL`, `URLSearchParams`, `TextEncoder`, `TextDecoder`, `performance`). `Goccia.Spec.pas` exposes a `WHATWG_FEATURES` array as `Goccia.spec.whatwg` on the runtime spec object. Do NOT flag `atob`/`btoa` or `structuredClone` as ECMAScript features (any edition) in future reviews.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 109
File: units/Goccia.Runtime.Operations.pas:2797-2828
Timestamp: 2026-03-23T15:57:21.755Z
Learning: Repo frostney/GocciaScript — PR `#109`: Native Map/Set scaffolding
- Current behavior: ToSouffleValue wraps TGocciaMapValue/TGocciaSetValue as TGocciaWrappedValue; TGocciaSouffleMap/TGocciaSouffleSet instances are not produced in normal execution.
- Pre-wired fast paths: GetProperty, GetIterator, and IteratorNext recognize TGocciaSouffleMap/TGocciaSouffleSet and their iterator types; these are staged for future activation.
- Deferred work: When Construct is updated to produce native collection types, add corresponding fast paths in HasProperty, SetProperty, and DeleteProperty.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 343
File: source/units/Goccia.Builtins.GlobalBigInt.pas:213-223
Timestamp: 2026-04-18T20:48:02.918Z
Learning: In `source/units/Goccia.Builtins.GlobalBigInt.pas` (frostney/GocciaScript, landed in commit 477b5609 on PR `#343`), `BigInt.asIntN` and `BigInt.asUintN` use proper `ToIndex` abstract-op coercion for the `bits` parameter: `undefined`/`NaN` → 0, `Infinity`/negative/oversized → `RangeError`. This is fully implemented and tested. Do not flag missing `ToIndex` coercion for these methods.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 351
File: tests/built-ins/global-properties/goccia-gc-memory-limit.js:85-106
Timestamp: 2026-04-19T15:42:42.176Z
Learning: In frostney/GocciaScript (PR `#351`, tests/built-ins/global-properties/goccia-gc-memory-limit.js), the `maxBytes vs suggestedMaxBytes` describe block is intentionally dual-mode: it derives `isOverridden = maxBytes !== suggestedMaxBytes` from observed runtime values and conditionally validates only the active path. This is by design — hard coverage for a broken `--max-memory` flag is provided by dedicated CI integration steps (in .github/workflows/pr.yml and ci.yml) that assert exact numeric output and exit codes. Do NOT re-flag this conditional pattern as hiding failures.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 286
File: units/Goccia.Spec.pas:30-51
Timestamp: 2026-04-12T15:33:54.442Z
Learning: In frostney/GocciaScript, `Proxy` is fully supported — it was implemented in PR `#226`. The `Proxy` entry in `ES2015_FEATURES` in `units/Goccia.Spec.pas` is correct and intentional. Do not flag `Proxy` as unimplemented in future reviews.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 58
File: units/Goccia.Runtime.Operations.pas:5959-6132
Timestamp: 2026-03-10T11:17:57.064Z
Learning: Repo: frostney/GocciaScript PR: 58
Context: units/Goccia.Runtime.Operations.pas (BridgePromiseStatic, NativePromiseStaticResolve, NativePromiseStaticReject) and interpreter Promise statics in Goccia.Builtins.GlobalPromise.pas
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.

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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 308
File: units/Goccia.VM.pas:0-0
Timestamp: 2026-04-15T15:41:55.194Z
Learning: In this repo’s Pascal code, when handling “assigning to a read-only” errors, use the exact suggestion type `SSuggestReadOnlyProperty`, paired with `SErrorCannotAssignReadOnly`. Keep this mapping consistent everywhere it applies (e.g., for `ObjectValue`, `ClassValue`, and similar value/descriptor assignments), so the same read-only assignment diagnostic always yields the same suggestion/supported error combination.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 333
File: source/shared/OrderedMap.Test.pas:387-401
Timestamp: 2026-04-18T03:14:07.382Z
Learning: For Free Pascal (FPC) in Delphi mode (`{$mode delphi}`) used in this repo (Pascal `.pas` files), do not treat a bare indexed-property read used as a standalone statement (e.g., `Map[999];`) as a compile error. In this mode, the getter call is executed and its result is silently discarded; if the getter raises an exception, it still propagates to the surrounding `except` handler as expected. For readability/portability, assigning the result to a dummy local (e.g., `Dummy := Map[999];`) is preferred, but bare indexed-property reads should remain unflagged as long as they are valid under FPC Delphi mode.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 337
File: source/units/Goccia.Builtins.TSV.pas:382-388
Timestamp: 2026-04-18T13:58:20.551Z
Learning: When reviewing GocciaScript Pascal code, do not flag missing `Args.Free`/`try/finally` around `TGocciaArgumentsCollection` instances. `TGocciaArgumentsCollection` (in `source/units/Goccia.Arguments.Collection.pas`) descends from `TInterfacedObject` and is reference-counted, so objects created via `CreateWithCapacity` or `Create` are automatically freed when the last reference goes out of scope. An explicit `Free` call (or wrapping in `try/finally`) is not appropriate for this type.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 351
File: source/units/Goccia.Engine.pas:1364-1368
Timestamp: 2026-04-19T15:42:49.575Z
Learning: In GocciaScript (e.g., source/units/Goccia.Engine.pas), treat `Goccia.gc()` / `TGocciaEngine.GocciaGC` as intentionally thread-safe: it should be a no-op on worker threads by guarding the manual `GC.Collect` call with `not GIsWorkerThread`. This prevents races on shared singleton state that uses a single, non-thread-safe `FGCMark` field during mark-sweep. Reviewers should not flag the `GIsWorkerThread` guard as a regression or as a missing manual-GC path; only the full/manual collection triggered by `Goccia.gc()` is skipped on workers, while other worker-thread protections (e.g., the memory-limit ceiling check in `TGocciaValue.AfterConstruction`) should still apply.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 354
File: source/units/Goccia.Values.TemporalPlainDate.pas:668-680
Timestamp: 2026-04-19T18:53:31.289Z
Learning: For this project (frostney/GocciaScript), do not raise review findings at individual call sites that use `Arg.ToStringLiteral.Value` on user-supplied arguments for missing Symbol guards (e.g., ensuring `Arg is TGocciaSymbolValue`). This implicit Symbol→string coercion gap is intentionally deferred to a project-wide fix that should be implemented in `ToStringLiteral` itself (or a shared coercion helper). Until that `ToStringLiteral` fix lands, suppress/skip re-flagging the same issue in methods like `DateWithCalendar` and other `withCalendar`/`withTimeZone`-style methods in `source/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.

Please create a follow up issue for that - marked for 0.12.0 milestone

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find milestone for 0.12.0
gh api repos/frostney/GocciaScript/milestones --jq '.[] | {number: .number, title: .title}'

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Create the follow-up issue
MILESTONE=$(gh api repos/frostney/GocciaScript/milestones --jq '.[] | select(.title == "0.12.0") | .number')
echo "Milestone number: $MILESTONE"

gh issue create \
  --repo frostney/GocciaScript \
  --title "Parser: ASI-aware recovery for unsupported \`var\` statement skip (--compat-var disabled)" \
  --body "## Summary

When \`--compat-var\` is **disabled**, \`TGocciaParser.VarStatement\` falls back to \`SkipUntilSemicolon\` to skip the unsupported \`var\` declaration before emitting a warning and returning \`TGocciaEmptyStatement\`. The current \`SkipUntilSemicolon\` implementation is not ASI-aware and will not stop on a closing \`}\` or EOF — it decrements the internal depth counter and can skip past closing braces, consuming more tokens than intended.

## Affected path

\`source/units/Goccia.Parser.pas\` — \`TGocciaParser.VarStatement\`, the \`not FVarDeclarationsEnabled\` branch (lines ~2777–2784).

## Expected behavior

The skip should stop on:
- An explicit \`;\`
- An ASI point (newline preceding a closing token or EOF)
- A \`}\` token (to avoid consuming closing braces)

## Notes

- This is only reachable when \`--compat-var\` is explicitly off, and in practice ASI is also typically disabled in that mode, so the current behavior is sufficient for the common case.
- The edge case (ASI enabled + \`--compat-var\` disabled + \`var\` statement with no explicit \`;\`) is a narrow combination but should be handled correctly.
- Backlinks: PR `#368`, comment https://github.com/frostney/GocciaScript/pull/368#discussion_r3112218334
- Reported by: `@frostney`
" \
  ${MILESTONE:+--milestone "$MILESTONE"}

Length of output: 65


🏁 Script executed:

#!/bin/bash
# Create issue without milestone first
ISSUE_URL=$(gh issue create \
  --repo frostney/GocciaScript \
  --title "Parser: ASI-aware recovery for unsupported \`var\` statement skip (--compat-var disabled)" \
  --body "## Summary

When \`--compat-var\` is **disabled**, \`TGocciaParser.VarStatement\` falls back to \`SkipUntilSemicolon\` to skip the unsupported \`var\` declaration before emitting a warning and returning \`TGocciaEmptyStatement\`. The current \`SkipUntilSemicolon\` implementation is not ASI-aware and will not stop on a closing \`}\` or EOF — it decrements the internal depth counter and can skip past closing braces, consuming more tokens than intended.

## Affected path

\`source/units/Goccia.Parser.pas\` — \`TGocciaParser.VarStatement\`, the \`not FVarDeclarationsEnabled\` branch (lines ~2777–2784).

## Expected behavior

The skip should stop on:
- An explicit \`;\`
- An ASI point (newline preceding a closing token or EOF)
- A \`}\` token (to avoid consuming closing braces)

## Notes

- This is only reachable when \`--compat-var\` is explicitly off, and in practice ASI is also typically disabled in that mode, so the current behavior is sufficient for the common case.
- The edge case (ASI enabled + \`--compat-var\` disabled + \`var\` statement with no explicit \`;\`) is a narrow combination but should be handled correctly.
- Backlinks: PR `#368`, comment https://github.com/frostney/GocciaScript/pull/368#discussion_r3112218334
- Reported by: `@frostney`
")
echo "Issue URL: $ISSUE_URL"

# Extract issue number from URL
ISSUE_NUM=$(echo "$ISSUE_URL" | grep -oE '[0-9]+$')
echo "Issue number: $ISSUE_NUM"

# Add milestone via API (milestone number is 9)
gh api --method PATCH "repos/frostney/GocciaScript/issues/$ISSUE_NUM" \
  --field milestone=9 \
  --jq '{number: .number, title: .title, milestone: .milestone.title, url: .html_url}'

Length of output: 274


@frostney, done! I've opened #371"Parser: ASI-aware recovery for unsupported var statement skip (--compat-var disabled)" — and assigned it to the 0.12.0 milestone.


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 368
File: source/units/Goccia.Evaluator.pas:1344-1355
Timestamp: 2026-04-20T19:42:50.788Z
Learning: In frostney/GocciaScript (`source/units/Goccia.Evaluator.pas` and `source/units/Goccia.Scope.pas`), var-scoped bindings are tracked in a dedicated `FVarBindings` map on the scope. `AssignBinding` uses this map to route writes to the correct hoisted var slot on the function/module scope without needing to swap `AContext.Scope` to the function scope. In for-of var destructuring (`EvaluateForOf`/`EvaluateForAwaitOf`), `AssignPattern(..., AIsDeclaration=False)` is used so that `AssignIdentifierPattern` calls `AssignBinding`, which traverses `FVarBindings` to find the hoisted binding, while computed keys and default values still resolve against the block scope. Do NOT add scope-swap patterns (redirecting `IterContext.Scope` to `FindFunctionOrModuleScope`) for var destructuring in loops; `AssignBinding` handles the scope-chain walk automatically via `FVarBindings`.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 368
File: source/units/Goccia.Compiler.Statements.pas:479-491
Timestamp: 2026-04-20T19:43:00.895Z
Learning: In frostney/GocciaScript (PR `#368`, commit 1098b659), the interpreter-side `var` redeclaration handling was refactored to use a dedicated `FVarBindings` map with a `DefineVarBinding(Name, Value, HasInitializer)` method. This method handles redeclaration semantics natively (preserving the existing value when `HasInitializer = false` and the binding already exists). The interpreter no longer uses `ForceUpdateBinding` or `ContainsOwnLexicalBinding` for `var` redeclarations. The compiler path (bytecode) uses `IsVarRedeclaration: Boolean` (set before `DeclareVarLocal` is called) to suppress `OP_LOAD_UNDEFINED` emission for re-declarations without real initializers; the compiler scope's `DeclareVarLocal` returns the existing slot on redeclaration and does NOT trigger the `IsTopLevelGlobalBacked` re-resolve path. Do NOT flag `LocalIdx` overwrite concerns in the `CompileVariableDeclaration` var-path — the `IsVarRedeclaration` boolean correctly captures the pre-declaration state.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T15:16:53.061Z
Learning: Applies to **/Goccia.Parser.pas : The parser must accept unsupported syntax but treat it as a no-op — `function` declarations produce `TGocciaEmptyStatement`, `function` expressions evaluate to `undefined`, `==`/`!=` expressions evaluate to `undefined`, labeled statements strip the label and execute the labeled statement normally. A warning must be emitted to stdout with a suggestion

Learnt from: frostney
Repo: frostney/GocciaScript PR: 368
File: source/units/Goccia.Parser.pas:2780-2787
Timestamp: 2026-04-20T16:59:47.231Z
Learning: In `source/units/Goccia.Parser.pas`, the `SkipUntilSemicolon` call in `TGocciaParser.VarStatement`'s unsupported-`var` warning path (when `FVarDeclarationsEnabled = False`) is intentionally not ASI-aware. This path is only reachable when `--compat-var` is disabled, and ASI is also typically disabled in that mode, making the semicolon-based skip sufficient for the common case. The edge-case interaction (ASI enabled + `--compat-var` disabled) is acknowledged as a known gap and deferred to a follow-up improvement. Do NOT re-flag `SkipUntilSemicolon` in this specific fallback path until that follow-up is addressed.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Parser.pas:949-961
Timestamp: 2026-04-11T09:44:42.181Z
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: 368
File: source/units/Goccia.Evaluator.pas:227-239
Timestamp: 2026-04-20T19:43:32.162Z
Learning: In frostney/GocciaScript (source/units/Goccia.Evaluator.pas), the `CollectVarNamesFromNode` hoisting pre-scan is intentionally scoped to `var` declarations lexically inside function/loop bodies (e.g. `var x` inside a for-of body). For the for-of loop binding itself (`for (var x of arr)`), frostney's design intent is that the binding is created at execution time via `DefineVarBinding` in the `EvaluateForOf` IsVar branch. However, this requires careful handling for empty iterables and pre-loop access: if no iterations occur, `x` is never bound, causing a ReferenceError instead of `undefined`. The fix in commit 38284eb4 pre-declares the loop binding name during the hoist scan to cover this edge case. Do NOT remove the `ForOf.IsVar` binding-collection block from `CollectVarNamesFromNode` without also ensuring empty-iterable and pre-loop-access behaviour is correct.

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: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-05T07:58:33.798Z
Learning: Applies to **/{Parser,AST}*.pas : type/interface declarations and import type/export type must produce TGocciaEmptyStatement (no-op at runtime)

Learnt from: frostney
Repo: frostney/GocciaScript PR: 368
File: source/units/Goccia.Compiler.Scope.pas:157-165
Timestamp: 2026-04-20T16:59:07.204Z
Learning: In `source/units/Goccia.Compiler.Scope.pas` (frostney/GocciaScript), `DeclareVarLocal` intentionally sets `Depth := 0` on newly declared var-hoisted locals. `EndScope` uses exact equality (`FLocals[FLocalCount-1].Depth = FDepth`) to pop locals, so depth-0 locals are never popped by any inner-scope unwind. `DeclareVarLocal` is only called during the hoisting pre-scan (before `BeginScope` increments depth) or from `CompileVariableDeclaration` where the var binding was already hoisted at depth 0. Do NOT flag `Depth := 0` in `DeclareVarLocal` as a scope-unwinding bug.

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: 302
File: docs/contributing/code-style.md:43-46
Timestamp: 2026-04-14T13:31:59.106Z
Learning: In frostney/GocciaScript (docs/contributing/code-style.md and related docs), units like `BaseMap.pas`, `HashMap.pas`, `OrderedMap.pas`, `OrderedStringMap.pas` are described as "shared infrastructure units that are not GocciaScript-specific" and intentionally live outside the `Goccia.*` namespace. They are NOT "legacy units." Only project-specific units follow the `Goccia.<Category>.<Name>.pas` convention. The naming convention for these shared infrastructure units is tracked in issue `#104`. Do not describe these units as "legacy" in review comments.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 58
File: units/Goccia.Runtime.Operations.pas:5959-6132
Timestamp: 2026-03-10T11:17:57.064Z
Learning: Repo: frostney/GocciaScript PR: 58
Context: units/Goccia.Runtime.Operations.pas (BridgePromiseStatic, NativePromiseStaticResolve, NativePromiseStaticReject) and interpreter Promise statics in Goccia.Builtins.GlobalPromise.pas
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.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 109
File: units/Goccia.Runtime.Operations.pas:6259-6271
Timestamp: 2026-03-23T15:59:01.630Z
Learning: Repo: frostney/GocciaScript — units/Goccia.Runtime.Operations.pas (PR `#109`)
As of PR `#109`, Map/Set instances are still wrapped as TGocciaWrappedValue by ToSouffleValue; no construction/wrapping path produces TGocciaSouffleMap/TGocciaSouffleSet yet. The native Map/Set fast paths (e.g., NativeMapDelete/Clear/ForEach) are dormant scaffolding and only trigger for Souffle-backed instances. Therefore, falling back to InvokeGocciaMethod for Map/Set is correct and safe today; do not re-flag this as an invalid cast or behavior until the construction path starts creating TGocciaSouffleMap/TGocciaSouffleSet instances.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 261
File: units/Goccia.Builtins.GlobalObject.pas:0-0
Timestamp: 2026-04-11T06:43:41.435Z
Learning: In frostney/GocciaScript (units/Goccia.Builtins.GlobalObject.pas), all Object.* static methods (Object.keys, Object.values, Object.entries, Object.getOwnPropertyNames, Object.getOwnPropertyDescriptors, Object.getOwnPropertyDescriptor, Object.assign, etc.) intentionally check `is TGocciaObjectValue` and throw for primitives instead of calling ToObject coercion. Primitive coercion (wrapping 42/"str"/true to their wrapper objects) is a known project-wide gap deferred to a dedicated follow-up PR. Do not flag individual Object.* methods for missing ToObject coercion until the project-wide fix lands.

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: 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: 0
File: :0-0
Timestamp: 2026-04-11T06:43:54.605Z
Learning: In frostney/GocciaScript, `ToObject` coercion for primitive arguments (e.g., passing a number or string to `Object.getOwnPropertyDescriptor`, `Object.getOwnPropertyDescriptors`, and other `Object.*` static methods) is intentionally deferred. This is a project-wide gap affecting 20+ `Object.*` methods in `units/Goccia.Builtins.GlobalObject.pas`, not specific to any single method. Do not flag missing `ToObject` coercion in individual `Object.*` method PRs as a blocking issue; reference this project-wide deferral instead.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 308
File: units/Goccia.VM.pas:3436-3437
Timestamp: 2026-04-15T15:41:51.609Z
Learning: Repo frostney/GocciaScript — units/Goccia.VM.pas:
For TGocciaVM.HasPropertyValue’s “in” operator error path (primitives on the RHS), the project intentionally uses SSuggestCheckNullBeforeAccess for all primitive cases (undefined, null, boolean, number, string). Branching the suggestion by value kind is deferred as low priority; do not re-flag this as an issue unless a dedicated follow-up changes that policy.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 354
File: source/units/Goccia.Values.TemporalPlainDate.pas:668-680
Timestamp: 2026-04-19T18:53:31.289Z
Learning: For this project (frostney/GocciaScript), do not raise review findings at individual call sites that use `Arg.ToStringLiteral.Value` on user-supplied arguments for missing Symbol guards (e.g., ensuring `Arg is TGocciaSymbolValue`). This implicit Symbol→string coercion gap is intentionally deferred to a project-wide fix that should be implemented in `ToStringLiteral` itself (or a shared coercion helper). Until that `ToStringLiteral` fix lands, suppress/skip re-flagging the same issue in methods like `DateWithCalendar` and other `withCalendar`/`withTimeZone`-style methods in `source/units`.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 308
File: units/Goccia.Values.TemporalPlainDateTime.pas:144-145
Timestamp: 2026-04-16T06:33:07.065Z
Learning: In frostney/GocciaScript Temporal unit implementations (e.g., units/Goccia.Values.TemporalPlainDateTime.pas, units/Goccia.Values.TemporalPlainDate.pas, units/Goccia.Values.TemporalPlainTime.pas, units/Goccia.Values.TemporalPlainYearMonth.pas, units/Goccia.Values.TemporalPlainMonthDay.pas, units/Goccia.Values.TemporalInstant.pas, units/Goccia.Values.TemporalZonedDateTime.pas, units/Goccia.Builtins.Temporal.pas), the correction of ThrowTypeError → ThrowRangeError for invalid ISO/time/duration string parsing failures (per TC39 Temporal spec) is intentionally deferred to a dedicated follow-up PR. PRs that only centralize error message constants and add suggestion metadata must not change error types (TypeError vs RangeError). Do not re-flag the ThrowTypeError usage for ISO/time/duration parse failures in these Temporal units until the dedicated behavioral-change PR lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T13:53:21.272Z
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: 324
File: units/Goccia.Values.TemporalDuration.pas:691-693
Timestamp: 2026-04-17T14:59:54.438Z
Learning: In frostney/GocciaScript (units/Goccia.Values.TemporalDuration.pas), TC39 Temporal Duration magnitude validation (calendar units < 2^32, normalized seconds < 2^53) is intentionally deferred to a follow-up PR. The validation belongs in the `TGocciaTemporalDurationValue` constructor and `DurationFromObject`, not in `DurationEndDate`, `AddMonthsToDate`, or `DurationTotal`. Do not re-flag missing magnitude validation in `DurationEndDate` or `total()` path — the arithmetic there is safe for spec-compliant inputs; the gap is upstream validation not yet implemented.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T06:46:13.250Z
Learning: In frostney/GocciaScript, `EscapeJSONString` (escaping `\`, `"`, `\n`, `\r`, `\t` only) is intentionally duplicated across ~5 CLI tool files (e.g., TestRunner.dpr, BenchmarkRunner.dpr). The incomplete control-character coverage (missing U+0000–U+001F beyond the five named characters) is a known pre-existing issue tracked for fix as part of a future consolidation into a shared helper. Do NOT re-flag missing control-character escaping in individual per-tool copies of `EscapeJSONString` until that consolidation PR lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 324
File: units/Goccia.Values.TemporalDuration.pas:630-656
Timestamp: 2026-04-17T14:39:13.057Z
Learning: In frostney/GocciaScript, `ParseRelativeTo` in `units/Goccia.Values.TemporalDuration.pas` intentionally supports only `TGocciaTemporalPlainDateValue` and ISO date/date-time string inputs for `relativeTo` in `Duration.prototype.total()`. Support for `TGocciaTemporalZonedDateTimeValue` and property-bag/object inputs is deferred to a dedicated follow-up PR, as it requires importing the ZonedDateTime unit, extracting the plain date, and handling DST-aware day lengths. Do not re-flag missing ZonedDateTime/property-bag handling in `ParseRelativeTo` until that follow-up PR lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 53
File: souffle/Souffle.Wasm.Emitter.pas:0-0
Timestamp: 2026-03-08T04:50:29.540Z
Learning: In format.pas (GocciaScript formatter), ParseParamNames previously misidentified parenthesized arguments inside one-liner function bodies as parameter names, causing the `A` prefix to be prepended to them. The fix (f5b2f23) makes ParseParamNames ignore parentheses appearing after the first semicolon. Be cautious when reviewing one-liner procedure/function bodies in .pas files for incorrectly prefixed identifiers — this was the root cause of the AGC_ANY_CONVERT_EXTERN → AGC_ANY_CONVERT_EXTERN typos in souffle/Souffle.Wasm.Emitter.pas.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 51
File: units/Goccia.AST.Expressions.pas:1022-1025
Timestamp: 2026-03-08T04:34:30.723Z
Learning: In units/Goccia.AST.Expressions.pas: `TGocciaTemplateWithInterpolationExpression` is never instantiated by the parser. All template literals (with and without interpolation) are parsed as `TGocciaTemplateLiteralExpression`. The `TemplateWithInterpolation` variant exists only for the bytecode compiler pipeline (`Goccia.Compiler.Expressions.pas`), which has its own dispatch. The `Evaluate` override returning `undefined` in the interpreter is intentional — it is unreachable code that preserves the pre-existing fall-through behavior. Do NOT flag this as a bug.

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 : Do not add support for these JavaScript features: `var` declarations, `function` keyword, `==` and `!=` loose equality, `eval()` and `arguments` object, automatic semicolon insertion, `with` statement, traditional loops (`for`, `while`, `do...while`), default imports/exports, or global `parseInt`, `parseFloat`, `isNaN`, `isFinite`. The parser may accept some unsupported syntax as no-ops with warnings, but these features must not be implemented.

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: 302
File: docs/language.md:118-118
Timestamp: 2026-04-14T13:18:31.918Z
Learning: In frostney/GocciaScript (units/Goccia.Lexer.pas, ScanTemplate / ProcessEscapeSequence), cooked=undefined support for tagged templates with malformed escape sequences (e.g., \u{, \xG1) landed in PR `#287` (TC39 Template Literal Revision / ES2018). Tagged templates now correctly set the cooked segment to undefined for malformed escapes while preserving the raw source text; untagged templates still throw SyntaxError. Do NOT re-flag this as missing or deferred.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 278
File: units/Goccia.Parser.pas:1010-1023
Timestamp: 2026-04-12T09:44:50.402Z
Learning: In frostney/GocciaScript (units/Goccia.Parser.pas, SplitTemplateAtBoundaries and the original ParseTaggedTemplate), the brace-counting used to find the closing `}` of a template interpolation expression (`${...}`) is NOT lexically aware — it does not account for `{`/`}` inside string literals, comments, or nested templates within the expression. This is a known pre-existing limitation that predates this codebase area; it was present in the original `ParseTaggedTemplate` on `main` and is carried forward. Fixing it properly requires lexer-time segmentation (embedding boundary markers during `ScanTemplate` while escape/string context is available). Do NOT re-flag the naive brace counter in SplitTemplateAtBoundaries or ParseTaggedTemplate as a regression or new bug — it is intentionally deferred to a follow-up. The limitation exists because `ScanTemplate` stores the entire template (including `${...}` expressions) as a single flat token with no expression-level lexical analysis.

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 annotations in GocciaScript (TC39 Types as Comments proposal) are parsed but ignored at runtime. Raw type strings are preserved on AST nodes for potential future optimization. Use parser helpers `CollectTypeAnnotation(Terminators)` and `CollectGenericParameters` for type handling.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 201
File: units/Goccia.Coverage.pas:225-249
Timestamp: 2026-04-07T20:24:40.351Z
Learning: In `units/Goccia.Coverage.pas` `BuildExecutableLineFlags`: `interface Foo {` opening lines are intentionally marked as executable (`AFlags[I] := True`) because they produce `TGocciaEmptyStatement` which the engine does evaluate; interface body lines are suppressed via `TypeBodyDepth`. Single-line `type` aliases (e.g. `type Point = { x: number };`) are also correctly counted as executable for the same reason. The only known gap is multi-line `type` aliases spanning several lines — those continuation lines are not currently suppressed, but this is considered rare in practice and acceptable for now. Do not flag the `interface` opening-line executable marking or single-line `type` handling as bugs.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 200
File: units/Goccia.Compiler.Statements.pas:569-570
Timestamp: 2026-04-07T17:12:43.685Z
Learning: In frostney/GocciaScript (units/Goccia.Compiler.Statements.pas, units/Goccia.Engine.Backend.pas, units/Goccia.Scope.Redeclaration.pas), the `EmitGlobalDefine` / `OP_DEFINE_GLOBAL_CONST` calls in `CompileVariableDeclaration`, `CompileClassDeclaration`, `CompileDestructuringDeclaration`, and `CompileEnumDeclaration` intentionally execute *after* the RHS/initializer has been evaluated. Redeclaration protection is handled up-front by `CheckTopLevelRedeclarations` (an AST-level pre-flight walk) called from `TGocciaBytecodeBackend.CompileToModule` (when `GlobalBackedTopLevel` is true) and from `TGocciaEngine.Execute` (after parsing, before execution). This catches all top-level `let`/`const`/class/enum redeclarations before any RHS side effects can run, matching V8 early-error behaviour. Do NOT re-flag the post-evaluation placement of `EmitGlobalDefine` as a side-effect ordering bug.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 343
File: source/units/Goccia.Evaluator.Arithmetic.pas:49-55
Timestamp: 2026-04-18T21:08:03.166Z
Learning: In frostney/GocciaScript (commit 3344dede, PR `#343`), `TGocciaBigIntObjectValue` was added to support BigInt boxing via `Object(1n)`. `Box` in `ClassHelper.pas` now returns a `TGocciaBigIntObjectValue` for `TGocciaBigIntValue` primitives. `ToPrimitive` calls were added to `EvaluateSubtraction`, `EvaluateMultiplication`, `EvaluateDivision`, `EvaluateModulo`, and `EvaluateExponentiation` in `source/units/Goccia.Evaluator.Arithmetic.pas` so that boxed BigInts (e.g. `Object(1n) - 1n`) are correctly unboxed via `valueOf()` before BigInt type checks. `Object()` was also fixed to call `Box` for primitive arguments per ES2026 §20.1.1.1. Future reviews of arithmetic operators SHOULD expect `ToPrimitive` before BigInt checks.

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: 109
File: units/Goccia.Runtime.Operations.pas:2797-2828
Timestamp: 2026-03-23T15:57:21.755Z
Learning: Repo frostney/GocciaScript — PR `#109`: Native Map/Set scaffolding
- Current behavior: ToSouffleValue wraps TGocciaMapValue/TGocciaSetValue as TGocciaWrappedValue; TGocciaSouffleMap/TGocciaSouffleSet instances are not produced in normal execution.
- Pre-wired fast paths: GetProperty, GetIterator, and IteratorNext recognize TGocciaSouffleMap/TGocciaSouffleSet and their iterator types; these are staged for future activation.
- Deferred work: When Construct is updated to produce native collection types, add corresponding fast paths in HasProperty, SetProperty, and DeleteProperty.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 276
File: tests/built-ins/Goccia/build.js:6-7
Timestamp: 2026-04-11T18:29:34.210Z
Learning: In frostney/GocciaScript, the pattern `const hasGoccia = typeof Goccia !== "undefined"` combined with `describe.runIf(hasGoccia)(...)` is intentional and used across the entire `tests/` suite. It allows test files to run safely in non-GocciaScript environments (e.g., Vitest compatibility checks) where GocciaScript-specific globals like `Goccia` are not present. Do NOT flag this conditional-skip guard as a regression risk or suggest making suites unconditional — the portability requirement is by design.

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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 308
File: units/Goccia.VM.pas:0-0
Timestamp: 2026-04-15T15:41:55.194Z
Learning: In this repo’s Pascal code, when handling “assigning to a read-only” errors, use the exact suggestion type `SSuggestReadOnlyProperty`, paired with `SErrorCannotAssignReadOnly`. Keep this mapping consistent everywhere it applies (e.g., for `ObjectValue`, `ClassValue`, and similar value/descriptor assignments), so the same read-only assignment diagnostic always yields the same suggestion/supported error combination.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 333
File: source/shared/OrderedMap.Test.pas:387-401
Timestamp: 2026-04-18T03:14:07.382Z
Learning: For Free Pascal (FPC) in Delphi mode (`{$mode delphi}`) used in this repo (Pascal `.pas` files), do not treat a bare indexed-property read used as a standalone statement (e.g., `Map[999];`) as a compile error. In this mode, the getter call is executed and its result is silently discarded; if the getter raises an exception, it still propagates to the surrounding `except` handler as expected. For readability/portability, assigning the result to a dummy local (e.g., `Dummy := Map[999];`) is preferred, but bare indexed-property reads should remain unflagged as long as they are valid under FPC Delphi mode.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 337
File: source/units/Goccia.Builtins.TSV.pas:382-388
Timestamp: 2026-04-18T13:58:20.551Z
Learning: When reviewing GocciaScript Pascal code, do not flag missing `Args.Free`/`try/finally` around `TGocciaArgumentsCollection` instances. `TGocciaArgumentsCollection` (in `source/units/Goccia.Arguments.Collection.pas`) descends from `TInterfacedObject` and is reference-counted, so objects created via `CreateWithCapacity` or `Create` are automatically freed when the last reference goes out of scope. An explicit `Free` call (or wrapping in `try/finally`) is not appropriate for this type.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 351
File: source/units/Goccia.Engine.pas:1364-1368
Timestamp: 2026-04-19T15:42:49.575Z
Learning: In GocciaScript (e.g., source/units/Goccia.Engine.pas), treat `Goccia.gc()` / `TGocciaEngine.GocciaGC` as intentionally thread-safe: it should be a no-op on worker threads by guarding the manual `GC.Collect` call with `not GIsWorkerThread`. This prevents races on shared singleton state that uses a single, non-thread-safe `FGCMark` field during mark-sweep. Reviewers should not flag the `GIsWorkerThread` guard as a regression or as a missing manual-GC path; only the full/manual collection triggered by `Goccia.gc()` is skipped on workers, while other worker-thread protections (e.g., the memory-limit ceiling check in `TGocciaValue.AfterConstruction`) should still apply.

Comment thread source/units/Goccia.Parser.pas
Comment thread tests/language/var/hoisting.js
- Fix hoisting to handle destructuring var declarations (walk patterns)
- Fix OP_LOAD_UNDEFINED not emitted for var redeclaration without initializer
- Add var hoisting to module body evaluation (imported modules)
- Propagate VarDeclarationsEnabled to nested parser in template interpolation
- Add IsVar to for-of statement for `for (var x of xs)` semantics
- Fix module-level hoisting test to actually be module-scoped
- Add --compat-var to CI/PR test commands
- Add test for var redeclaration without initializer preserving value

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

Caution

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

⚠️ Outside diff range comments (1)
source/units/Goccia.Evaluator.pas (1)

1437-1440: ⚠️ Potential issue | 🟠 Major

Missing IsVar handling in EvaluateForAwaitOf.

EvaluateForOf was updated with IsVar support (lines 1339-1355), but EvaluateForAwaitOf lacks equivalent handling. for await (var x of asyncIterable) will treat bindings as lexical instead of function-scoped.

The same pattern needs to be applied at both binding sites (lines 1437-1440 for async iterators and lines 1478-1481 for sync fallback).

🔧 Proposed fix for async iterator path
-          if AForAwaitOfStatement.BindingPattern <> nil then
-            AssignPattern(AForAwaitOfStatement.BindingPattern, CurrentValue, IterContext, True, DeclarationType)
-          else
-            IterScope.DefineLexicalBinding(AForAwaitOfStatement.BindingName, CurrentValue, DeclarationType);
+          if AForAwaitOfStatement.IsVar then
+          begin
+            if AForAwaitOfStatement.BindingPattern <> nil then
+            begin
+              IterContext.Scope := AContext.Scope.FindFunctionOrModuleScope;
+              AssignPattern(AForAwaitOfStatement.BindingPattern, CurrentValue, IterContext, False);
+              IterContext.Scope := IterScope;
+            end
+            else
+              AContext.Scope.FindFunctionOrModuleScope.ForceUpdateBinding(
+                AForAwaitOfStatement.BindingName, CurrentValue);
+          end
+          else if AForAwaitOfStatement.BindingPattern <> nil then
+            AssignPattern(AForAwaitOfStatement.BindingPattern, CurrentValue, IterContext, True, DeclarationType)
+          else
+            IterScope.DefineLexicalBinding(AForAwaitOfStatement.BindingName, CurrentValue, DeclarationType);

Apply the same pattern to the sync fallback path at lines 1478-1481.

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

In `@source/units/Goccia.Evaluator.pas` around lines 1437 - 1440,
EvaluateForAwaitOf is missing IsVar handling so bindings from "for await" are
always treated as lexical; update the two binding sites in EvaluateForAwaitOf
(the async iterator path where AForAwaitOfStatement.BindingPattern / BindingName
are handled and the sync fallback path) to mirror the logic added in
EvaluateForOf: when AForAwaitOfStatement.IsVar is true define function-scoped
bindings (use IterScope.DefineBinding or the equivalent function-scoped define
call used in EvaluateForOf) instead of IterScope.DefineLexicalBinding, and when
a pattern is present call AssignPattern with the same IsVar-aware arguments
(passing CurrentValue, IterContext, True, DeclarationType/IsVar behavior
consistent with EvaluateForOf). Ensure both the async path (where AssignPattern
/ DefineLexicalBinding are invoked) and the sync fallback path receive the same
IsVar-aware branching so "for await (var x of ...)" is function-scoped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/units/Goccia.Compiler.Statements.pas`:
- Around line 479-489: The code overwrites LocalIdx after saving a redeclaration
check, so the subsequent logic (e.g., emitting OP_LOAD_UNDEFINED) sees the wrong
value; add a dedicated boolean (e.g., WasRedeclaration) set right after LocalIdx
:= ACtx.Scope.ResolveLocal(Info.Name) / immediately after calling
ACtx.Scope.DeclareVarLocal(Info.Name) when AStmt.IsVar, preserve that boolean
through the IsTopLevelGlobalBacked branch where LocalIdx may be reassigned via
ResolveLocal, and replace uses of LocalIdx >= 0 in the later checks (the
conditions around OP_LOAD_UNDEFINED and the checks at the locations referenced)
with the new boolean (or use both where appropriate) so the original
redeclaration state is not lost by Declare/ResolveLocal calls.

In `@source/units/Goccia.Evaluator.pas`:
- Around line 1339-1355: The var-destructuring branch uses AssignPattern with
AIsDeclaration=True causing lexical-style defines; change it to use assignment
semantics by calling AssignPattern with AIsDeclaration=False when handling
AForOfStatement.IsVar (inside the block that sets IterContext.Scope :=
AContext.Scope.FindFunctionOrModuleScope), mirroring the simple binding case
which uses ForceUpdateBinding; update the call that currently passes True to
instead pass False so AssignPattern performs updates rather than
DefineLexicalBinding.
- Around line 227-232: The for-of branch misses hoisting loop-declared vars:
when ANode is TGocciaForOfStatement and ForOf.IsVar is true you must also
collect binding names from the loop's BindingName or BindingPattern before
traversing the body; update the TGocciaForOfStatement branch to, if ForOf.IsVar,
call CollectVarNamesFromNode on ForOf.BindingName (or ForOf.BindingPattern when
present) in addition to CollectVarNamesFromNode(ForOf.Body) so var bindings like
for(var x of y) get pre-declared into ANames.

---

Outside diff comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 1437-1440: EvaluateForAwaitOf is missing IsVar handling so
bindings from "for await" are always treated as lexical; update the two binding
sites in EvaluateForAwaitOf (the async iterator path where
AForAwaitOfStatement.BindingPattern / BindingName are handled and the sync
fallback path) to mirror the logic added in EvaluateForOf: when
AForAwaitOfStatement.IsVar is true define function-scoped bindings (use
IterScope.DefineBinding or the equivalent function-scoped define call used in
EvaluateForOf) instead of IterScope.DefineLexicalBinding, and when a pattern is
present call AssignPattern with the same IsVar-aware arguments (passing
CurrentValue, IterContext, True, DeclarationType/IsVar behavior consistent with
EvaluateForOf). Ensure both the async path (where AssignPattern /
DefineLexicalBinding are invoked) and the sync fallback path receive the same
IsVar-aware branching so "for await (var x of ...)" is function-scoped.
🪄 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: baf123ca-8910-439d-a627-92ed2bcb833f

📥 Commits

Reviewing files that changed from the base of the PR and between ca316e6 and fe6a45e.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .github/workflows/pr.yml
  • source/units/Goccia.AST.Statements.pas
  • source/units/Goccia.Compiler.Statements.pas
  • source/units/Goccia.Compiler.pas
  • source/units/Goccia.Evaluator.pas
  • source/units/Goccia.Interpreter.pas
  • source/units/Goccia.Parser.pas
  • tests/language/var/hoisting.js
  • tests/language/var/redeclaration.js
✅ Files skipped from review due to trivial changes (1)
  • tests/language/var/redeclaration.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/language/var/hoisting.js
  • source/units/Goccia.Compiler.pas

Comment thread source/units/Goccia.Compiler.Statements.pas
Comment thread source/units/Goccia.Evaluator.pas
Comment thread source/units/Goccia.Evaluator.pas
frostney and others added 2 commits April 20, 2026 18:26
Instead of requiring --compat-var on the global CLI (which conflicts
with the unsupported-features tests), the TestRunner now discovers
goccia.json per test file. Files in tests/language/var/ pick up
{"compat-var": true} from their directory's config automatically.

Reverts the --compat-var addition to CI/PR workflow commands since
it is no longer needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use dedicated IsVarRedeclaration boolean instead of LocalIdx which
  gets overwritten by later ResolveLocal calls
- Collect for-of loop binding names during var hoisting scan
- Use assignment semantics (IsDeclaration=False) for var destructuring
  in for-of loops since bindings are already hoisted

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

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

Inline comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 1346-1359: In EvaluateForAwaitOf replicate the same var semantics
used in EvaluateForOf: when AForAwaitOfStatement.IsVar is true, set
IterContext.Scope to AContext.Scope.FindFunctionOrModuleScope and call
AssignPattern(AForAwaitOfStatement.BindingPattern, CurrentValue, IterContext,
False) for destructuring or use
FindFunctionOrModuleScope.ForceUpdateBinding(AForAwaitOfStatement.BindingName,
CurrentValue) for a simple name; reset IterContext.Scope back to IterScope
afterwards. Otherwise (not IsVar) keep the existing behavior of binding through
IterScope. Update both binding sites around the lines that currently always use
IterScope so for await(var ...) hoists to function/module scope consistently.
- Around line 265-267: The TStringList used to collect hoist names (Names) is
case-insensitive by default, causing distinct identifiers like "foo" and "Foo"
to be merged; after creating the list (TStringList.Create) set
Names.CaseSensitive := True before calling CollectVarNames or performing
IndexOf/deduplication, and apply the same change to the other TStringList
instance used for hoisting at the 281-283 region so all hoist/name-collection
lists are case-sensitive.
🪄 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: 9f2d7af1-8a5b-4569-a468-8f186ad3bfad

📥 Commits

Reviewing files that changed from the base of the PR and between db3405d and 38284eb.

📒 Files selected for processing (2)
  • source/units/Goccia.Compiler.Statements.pas
  • source/units/Goccia.Evaluator.pas

Comment thread source/units/Goccia.Evaluator.pas
Comment thread source/units/Goccia.Evaluator.pas
Both the async iterator path and sync fallback path now mirror the
IsVar-aware branching from EvaluateForOf: when IsVar is true, bindings
are defined in the function scope via ForceUpdateBinding (simple name)
or AssignPattern with IsDeclaration=False (destructuring pattern).

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

♻️ Duplicate comments (1)
source/units/Goccia.Evaluator.pas (1)

265-267: ⚠️ Potential issue | 🟠 Major

Make the hoist-name set case-sensitive.

Names.IndexOf(...) is doing JavaScript identifier dedupe here, but these TStringList instances still use the default case-insensitive comparison. That merges distinct bindings like foo and Foo, so only one name gets hoisted/predeclared.

Suggested fix
 procedure HoistVarDeclarations(const AStatements: TObjectList<TGocciaStatement>; const AScope: TGocciaScope);
 var
   Names: TStringList;
   I: Integer;
 begin
   Names := TStringList.Create;
+  Names.CaseSensitive := True;
   try
     CollectVarNames(AStatements, Names);
 procedure HoistVarDeclarations(const ANodes: TObjectList<TGocciaASTNode>; const AScope: TGocciaScope);
 var
   Names: TStringList;
   I: Integer;
 begin
   Names := TStringList.Create;
+  Names.CaseSensitive := True;
   try
     for I := 0 to ANodes.Count - 1 do

Also applies to: 281-283

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

In `@source/units/Goccia.Evaluator.pas` around lines 265 - 267, The TStringList
"Names" is created with default case-insensitive comparison so IndexOf merges
identifiers like "foo" and "Foo"; after creating Names (and likewise any other
TStringList created near lines 281–283), set its CaseSensitive property to True
before calling CollectVarNames or using IndexOf so hoisting/predeclaration
treats distinct-case identifiers separately (i.e., set Names.CaseSensitive :=
True immediately after Names := TStringList.Create and do the same for the other
TStringList instances).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 1351-1353: The current fix replaces the entire evaluation scope
(IterContext.Scope / VarContext.Scope) with the function/module scope before
calling AssignPattern, which causes computed keys and default initializers to
resolve against the wrong scope; instead preserve the existing evaluation
context and only redirect the binding target: restore
IterContext.Scope/VarContext.Scope to the original value for expression
evaluation, and pass the function/module scope as the explicit target scope to
AssignPattern (i.e., use a separate target scope variable such as
IterScope/VarScope rather than assigning into IterContext.Scope), adjusting
calls to AssignPattern at the sites using IterContext.Scope :=
AContext.Scope.FindFunctionOrModuleScope (and the analogous changes at the other
mentioned locations) so evaluation uses the original scope while identifier
writes go to the function/module scope.

---

Duplicate comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 265-267: The TStringList "Names" is created with default
case-insensitive comparison so IndexOf merges identifiers like "foo" and "Foo";
after creating Names (and likewise any other TStringList created near lines
281–283), set its CaseSensitive property to True before calling CollectVarNames
or using IndexOf so hoisting/predeclaration treats distinct-case identifiers
separately (i.e., set Names.CaseSensitive := True immediately after Names :=
TStringList.Create and do the same for the other TStringList instances).
🪄 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: 53da0a5b-16dd-4605-afe6-8c5ef21b3606

📥 Commits

Reviewing files that changed from the base of the PR and between 38284eb and 6f64c12.

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

Comment thread source/units/Goccia.Evaluator.pas Outdated
frostney and others added 2 commits April 20, 2026 18:50
Remove scope swaps (IterContext.Scope := FindFunctionOrModuleScope)
before calling AssignPattern for var destructuring. Since we use
IsDeclaration=False, AssignLexicalBinding walks the parent chain to
find the hoisted binding in the function scope. Keeping the block
scope as the evaluation context ensures computed keys and default
initializers resolve against the correct lexical environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Architectural rework: var bindings now live in a dedicated FVarBindings
map on function/module/global scopes, separate from the lexical binding
system. This eliminates the impedance mismatch between var semantics
(function-scoped, redeclarable, no TDZ) and lexical semantics
(block-scoped, no redeclaration, TDZ).

Changes:
- Add dtVar to TGocciaDeclarationType (always writable, no TDZ)
- Add FVarBindings: TGocciaScopeBindingMap to TGocciaScope (lazily allocated)
- Add DefineVarBinding(Name, Value, HasInitializer) API that handles
  create-or-update semantics and redeclaration preservation natively
- Modify GetLexicalBinding to check FVarBindings at each scope level
- Modify AssignLexicalBinding to check FVarBindings before parent walk
- Update MarkReferences and Contains for GC and lookup correctness
- Simplify TGocciaVariableDeclaration.Execute to just call DefineVarBinding
- Simplify HoistVarDeclarations to populate the var map directly
- Replace ForceUpdateBinding/FindFunctionOrModuleScope hacks in for-of
  with clean DefineVarBinding calls

This removes the need for:
- ForceUpdateBinding for var redeclaration
- ContainsOwnLexicalBinding guards before DefineLexicalBinding
- IsDeclaration=False parent-chain-walk trick for destructuring
- Scope swaps (IterContext.Scope := FindFunctionOrModuleScope)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…/AssignIdentifierBinding

These methods resolve an identifier name to its binding across the full
scope chain, checking both lexical (let/const) and var binding maps.
The name "Identifier" makes it clear these resolve a named identifier
regardless of binding type.

The define/create operations remain properly separated by binding kind:
- DefineLexicalBinding — creates let/const/parameter in lexical map
- DefineVarBinding — creates var in var map

The identifier resolution operations check both maps:
- GetIdentifierBinding — walks chain, checks lexical + var at each scope
- AssignIdentifierBinding — walks chain, updates in lexical or var map

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
frostney and others added 3 commits April 20, 2026 22:12
…ding

Rename the binding resolution functions to reflect that they resolve any
binding type (lexical or variable), not just lexical bindings:

- GetIdentifierBinding -> GetBinding
- AssignIdentifierBinding -> AssignBinding
- DefineVarBinding -> DefineVariableBinding

The naming now follows a clear pattern:
- DefineLexicalBinding — creates a let/const/parameter binding
- DefineVariableBinding — creates a var binding
- GetBinding — resolves any binding by name (walks scope chain)
- AssignBinding — assigns to any binding by name (walks scope chain)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all `AssignPattern(..., False)` calls in var paths with a
dedicated `DefineVariablePattern` procedure that walks destructuring
patterns and calls `DefineVariableBinding` for each leaf identifier.

This fixes the case where `for (var {a, b} of arr)` would fail because
the loop binding names aren't pre-hoisted — `AssignPattern` with
`IsDeclaration=False` calls `AssignBinding` which throws if the name
doesn't exist. `DefineVariablePattern` calls `DefineVariableBinding`
which creates-or-updates, so it works correctly on first iteration
without requiring prior hoisting of the loop binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follows GocciaScript's Define vs Assign convention: Define creates new
bindings, Assign updates existing ones. The pattern walker assigns
destructured values into variable bindings (which may already exist
from hoisting or prior iterations), so Assign is the correct verb.

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

♻️ Duplicate comments (1)
source/units/Goccia.Evaluator.pas (1)

262-290: ⚠️ Potential issue | 🟠 Major

Use a case-sensitive name set while hoist-deduping.

These TStringList instances still rely on default IndexOf semantics while deduplicating JS identifiers. That merges distinct names like foo and Foo, so only one gets predeclared and the other stays unhoisted.

Suggested fix
 procedure HoistVarDeclarations(const AStatements: TObjectList<TGocciaStatement>; const AScope: TGocciaScope);
 var
   Names: TStringList;
   I: Integer;
 begin
   Names := TStringList.Create;
+  Names.CaseSensitive := True;
   try
     CollectVarNames(AStatements, Names);
 procedure HoistVarDeclarations(const ANodes: TObjectList<TGocciaASTNode>; const AScope: TGocciaScope);
 var
   Names: TStringList;
   I: Integer;
 begin
   Names := TStringList.Create;
+  Names.CaseSensitive := True;
   try
     for I := 0 to ANodes.Count - 1 do
In Free Pascal / Delphi RTL, does TStringList.IndexOf use case-insensitive matching by default unless CaseSensitive := True?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/units/Goccia.Evaluator.pas` around lines 262 - 290, The TStringList
used for collecting/deduping variable names in both HoistVarDeclarations
routines is case-insensitive by default; change the creation of Names in each
HoistVarDeclarations (the TStringList variable named Names) to enable
case-sensitive comparison (e.g. after Names := TStringList.Create set
Names.CaseSensitive := True) so identifiers like "foo" and "Foo" are treated as
distinct when collecting and defining bindings in AScope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 3929-3957: The TGocciaArrayDestructuringPattern branch only
handles TGocciaArrayValue, causing string and iterable sources to be treated as
undefined; update this branch (inside the APattern is
TGocciaArrayDestructuringPattern handling) to mirror AssignArrayPattern's logic:
detect when AValue is a string or a generic iterable/iterator (in addition to
TGocciaArrayValue), convert those into a sequence you can index/iterate (e.g.,
build a temporary TGocciaArrayValue or enumerate an iterator into elements),
then proceed with the same loop that handles Rest patterns and per-index
assignment via AssignVariablePattern; ensure fallback to
TGocciaUndefinedLiteralValue when an index is out of range and reuse the same
symbols TGocciaArrayDestructuringPattern, TGocciaArrayValue,
TGocciaUndefinedLiteralValue, and AssignVariablePattern so string/iterator
sources produce the same hoisted bindings as arrays.
- Around line 3916-3927: The object-destructuring branch in
AssignVariablePattern (handling TGocciaObjectDestructuringPattern) must mirror
AssignObjectPattern: first guard that AValue is an object and not null and raise
a JS TypeError if not; when iterating ObjPat.Properties handle computed keys
that evaluate to Symbol (use the evaluated key as-is rather than
ToStringLiteral) so symbol-keyed properties work like let/const; and implement
object-rest collection by building a new object with properties from AValue
excluding all assigned keys and then AssignVariablePattern on ObjPat.Rest (or
equivalent rest field) with that object. Ensure you reuse the same helper calls
and property-key extraction logic used by AssignObjectPattern to keep semantics
identical.

---

Duplicate comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 262-290: The TStringList used for collecting/deduping variable
names in both HoistVarDeclarations routines is case-insensitive by default;
change the creation of Names in each HoistVarDeclarations (the TStringList
variable named Names) to enable case-sensitive comparison (e.g. after Names :=
TStringList.Create set Names.CaseSensitive := True) so identifiers like "foo"
and "Foo" are treated as distinct when collecting and defining bindings in
AScope.
🪄 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: 1f9016da-b45f-4ff3-8a8c-5d4ffa484bcc

📥 Commits

Reviewing files that changed from the base of the PR and between 38284eb and 1c1e276.

📒 Files selected for processing (6)
  • source/units/Goccia.AST.Expressions.pas
  • source/units/Goccia.AST.Statements.pas
  • source/units/Goccia.Evaluator.pas
  • source/units/Goccia.Scope.BindingMap.pas
  • source/units/Goccia.Scope.pas
  • source/units/Goccia.VM.pas
🚧 Files skipped from review as they are similar to previous changes (1)
  • source/units/Goccia.AST.Statements.pas

Comment on lines +3916 to +3927
else if APattern is TGocciaObjectDestructuringPattern then
begin
ObjPat := TGocciaObjectDestructuringPattern(APattern);
for I := 0 to ObjPat.Properties.Count - 1 do
begin
if ObjPat.Properties[I].Computed and Assigned(ObjPat.Properties[I].KeyExpression) then
PropValue := (AValue as TGocciaObjectValue).GetProperty(
EvaluateExpression(ObjPat.Properties[I].KeyExpression, AContext).ToStringLiteral.Value)
else
PropValue := (AValue as TGocciaObjectValue).GetProperty(ObjPat.Properties[I].Key);
AssignVariablePattern(ObjPat.Properties[I].Pattern, PropValue, AContext);
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mirror AssignObjectPattern semantics in the var helper.

This object branch skips the non-object/null guard, symbol-key handling, and object-rest collection that AssignObjectPattern already implements. That makes cases like var {...rest} = obj and var {[sym]: x} = obj behave differently from the let/const path, and a non-object RHS can surface a raw cast failure instead of the expected JS TypeError. Because EvaluateDestructuringDeclaration, EvaluateForOf, and EvaluateForAwaitOf all route var patterns through this helper, the regression is broad.

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

In `@source/units/Goccia.Evaluator.pas` around lines 3916 - 3927, The
object-destructuring branch in AssignVariablePattern (handling
TGocciaObjectDestructuringPattern) must mirror AssignObjectPattern: first guard
that AValue is an object and not null and raise a JS TypeError if not; when
iterating ObjPat.Properties handle computed keys that evaluate to Symbol (use
the evaluated key as-is rather than ToStringLiteral) so symbol-keyed properties
work like let/const; and implement object-rest collection by building a new
object with properties from AValue excluding all assigned keys and then
AssignVariablePattern on ObjPat.Rest (or equivalent rest field) with that
object. Ensure you reuse the same helper calls and property-key extraction logic
used by AssignObjectPattern to keep semantics identical.

Comment thread source/units/Goccia.Evaluator.pas
Documentation:
- Update language.md: var is now "opt-in" with feature documentation
- Update language-tables.md: var status changed from "Excluded" to "Opt-in"
- Add decision-log.md entry explaining the architectural choice

Code fixes from review:
- Set CaseSensitive := True on TStringList in HoistVarDeclarations
- Add null/undefined guards to AssignVariablePattern
- Add string source handling to AssignVariablePattern array branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation internal Refactoring, CI, tooling, cleanup labels Apr 20, 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.

Caution

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

⚠️ Outside diff range comments (1)
docs/language.md (1)

9-9: ⚠️ Potential issue | 🟡 Minor

Update Executive Summary to reflect var as opt-in.

Line 9 lists var under "Excluded by design," but the detailed section (line 566) now describes it as "Opt-in. Excluded by default" with --compat-var support. This creates an inconsistency. Consider moving var out of the "Excluded by design" bullet or rephrasing to clarify it's available as a compatibility mode.

📝 Suggested revision
-- **Excluded by design** — `var`, `function` keyword, `==`/`!=`, `eval`, `arguments`, traditional loops, `with`, default imports/exports
+- **Excluded by design** — `function` keyword, `==`/`!=`, `eval`, `arguments`, traditional loops, `with`, default imports/exports
+- **Opt-in compatibility** — `var` (via `--compat-var`), ASI (via `--asi`)

Or alternatively, update line 11:

-- **Opt-in toggles** — ASI (`--asi` / `Engine.ASIEnabled := True`)
+- **Opt-in toggles** — ASI (`--asi` / `Engine.ASIEnabled := True`), `var` declarations (`--compat-var` / `Engine.VarEnabled := True`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/language.md` at line 9, Update the Executive Summary's "Excluded by
design" bullet so it no longer lists `var` as wholly excluded; instead reflect
that `var` is excluded by default but available opt-in via the `--compat-var`
compatibility flag (or move `var` out of that bullet and add a parenthetical
note about `--compat-var`), ensuring the summary matches the detailed section
that describes `var` as "Opt-in. Excluded by default."
♻️ Duplicate comments (2)
source/units/Goccia.Evaluator.pas (2)

3918-3933: ⚠️ Potential issue | 🟠 Major

Mirror AssignObjectPattern semantics in the var helper.

This branch still diverges from AssignObjectPattern: only null/undefined are guarded, so non-object RHS values can still blow up on the cast at Line 3928; computed symbol keys are coerced to strings; and object rest is never collected. That breaks cases like var {[sym]: x} = obj and var {...rest} = obj.

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

In `@source/units/Goccia.Evaluator.pas` around lines 3918 - 3933, The var-branch
for TGocciaObjectDestructuringPattern must mirror AssignObjectPattern: first
guard against any non-object RHS (not just null/undefined) before casting AValue
to TGocciaObjectValue; for computed property keys use the same
ToPropertyKey/coercion path used in AssignObjectPattern (evaluate the
KeyExpression and convert to a property key instead of ToStringLiteral.Value) so
symbols are handled correctly; and implement collection of the object rest
properties into the rest pattern (handle TGocciaObjectRestProperty like
AssignObjectPattern does) before assigning individual properties. Update the
branch around TGocciaObjectDestructuringPattern, referencing ObjPat, GetProperty
and AssignVariablePattern to follow the same checks and helper calls used by
AssignObjectPattern.

3935-3994: ⚠️ Potential issue | 🟠 Major

Keep var array destructuring iterable-aware.

Unlike AssignArrayPattern, this path never falls back to GetIteratorFromValue, so var [x] = someIterable() and for (var [x] of someIterator()) silently leave the hoisted binding at undefined instead of consuming the iterator. The array-rest branch also copies raw holes, while the lexical path normalizes them to undefined.

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

In `@source/units/Goccia.Evaluator.pas` around lines 3935 - 3994, The
TGocciaArrayDestructuringPattern branch currently only handles TGocciaArrayValue
and TGocciaStringLiteralValue and therefore fails to consume generic iterables;
update the else branch in the TGocciaArrayDestructuringPattern handling so that
when AValue is neither TGocciaArrayValue nor TGocciaStringLiteralValue you call
GetIteratorFromValue(AValue, AContext) and iterate it to produce element values
exactly as AssignArrayPattern does (including producing
TGocciaUndefinedLiteralValue for holes and collecting remaining iterator results
into a TGocciaArrayValue for TGocciaRestDestructuringPattern), then call
AssignVariablePattern for each element/rest argument; ensure the rest branch
uses the iterator (not raw holes) and normalizes holes to undefined so hoisted
var bindings are consumed and mirror AssignArrayPattern behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@docs/language.md`:
- Line 9: Update the Executive Summary's "Excluded by design" bullet so it no
longer lists `var` as wholly excluded; instead reflect that `var` is excluded by
default but available opt-in via the `--compat-var` compatibility flag (or move
`var` out of that bullet and add a parenthetical note about `--compat-var`),
ensuring the summary matches the detailed section that describes `var` as
"Opt-in. Excluded by default."

---

Duplicate comments:
In `@source/units/Goccia.Evaluator.pas`:
- Around line 3918-3933: The var-branch for TGocciaObjectDestructuringPattern
must mirror AssignObjectPattern: first guard against any non-object RHS (not
just null/undefined) before casting AValue to TGocciaObjectValue; for computed
property keys use the same ToPropertyKey/coercion path used in
AssignObjectPattern (evaluate the KeyExpression and convert to a property key
instead of ToStringLiteral.Value) so symbols are handled correctly; and
implement collection of the object rest properties into the rest pattern (handle
TGocciaObjectRestProperty like AssignObjectPattern does) before assigning
individual properties. Update the branch around
TGocciaObjectDestructuringPattern, referencing ObjPat, GetProperty and
AssignVariablePattern to follow the same checks and helper calls used by
AssignObjectPattern.
- Around line 3935-3994: The TGocciaArrayDestructuringPattern branch currently
only handles TGocciaArrayValue and TGocciaStringLiteralValue and therefore fails
to consume generic iterables; update the else branch in the
TGocciaArrayDestructuringPattern handling so that when AValue is neither
TGocciaArrayValue nor TGocciaStringLiteralValue you call
GetIteratorFromValue(AValue, AContext) and iterate it to produce element values
exactly as AssignArrayPattern does (including producing
TGocciaUndefinedLiteralValue for holes and collecting remaining iterator results
into a TGocciaArrayValue for TGocciaRestDestructuringPattern), then call
AssignVariablePattern for each element/rest argument; ensure the rest branch
uses the iterator (not raw holes) and normalizes holes to undefined so hoisted
var bindings are consumed and mirror AssignArrayPattern behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78740ab6-cb1e-45a2-b81c-333f6119e7ed

📥 Commits

Reviewing files that changed from the base of the PR and between 1c1e276 and 2a704d0.

📒 Files selected for processing (4)
  • docs/decision-log.md
  • docs/language-tables.md
  • docs/language.md
  • source/units/Goccia.Evaluator.pas
✅ Files skipped from review due to trivial changes (2)
  • docs/decision-log.md
  • docs/language-tables.md

- Resolve decision-log.md conflict (keep both entries)
- Update executive summary: move var from "excluded" to opt-in toggles
- Add generic iterable fallback to AssignVariablePattern array branch
  (mirrors AssignArrayPattern for iterators beyond Array/String)
- Add Iterator/IterResult vars to AssignVariablePattern

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.

Caution

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

⚠️ Outside diff range comments (1)
source/units/Goccia.Compiler.Statements.pas (1)

2797-2811: ⚠️ Potential issue | 🟠 Major

Top-level global-backed var destructuring bindings are never marked or emitted.

When AStmt.IsVar destructuring is compiled at global scope with GlobalBackedTopLevel = true, CollectDestructuringVarBindings calls DeclareVarLocal for each identifier. Since DeclareVarLocal is idempotent (returns existing slot on redeclaration), and hoisting has already pre-declared all identifiers during DoCompileFunctionBody, the second call in CompileDestructuringDeclaration adds no new locals. Thus LocalCountBefore == ACtx.Scope.LocalCount after the call, leaving both MarkGlobalBacked and EmitGlobalDefine loops with an empty iteration range. The hoisted bindings are skipped entirely.

CompileVariableDeclaration avoids this by re-resolving each binding after the slot allocation path completes (rather than relying on a limited range). CompileDestructuringDeclaration requires the same treatment: re-resolve each leaf identifier in the pattern and call MarkGlobalBacked and EmitGlobalDefine individually, matching the single-variable path.

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

In `@source/units/Goccia.Compiler.Statements.pas` around lines 2797 - 2811,
CompileDestructuringDeclaration currently assumes new locals appear in the
ACtx.Scope range between LocalCountBefore and ACtx.Scope.LocalCount, but when
hoisting/DeclareVarLocal pre-declared slots this range can be empty and
top-level var destructuring globals are skipped; change the end of
CompileDestructuringDeclaration (after EmitDestructuring/FreeRegister) to walk
the destructuring pattern's leaf identifiers and for each identifier re-resolve
its Local via the same lookup used in CompileVariableDeclaration, then call
ACtx.Scope.MarkGlobalBacked(LocalIndex) and EmitGlobalDefine(ACtx, Local.Slot,
Local.Name, AStmt.IsConst) for each leaf; reference functions/idents:
CompileDestructuringDeclaration, CollectDestructuringVarBindings,
DeclareVarLocal, DoCompileFunctionBody, EmitDestructuring,
ACtx.Scope.MarkGlobalBacked, EmitGlobalDefine.
🧹 Nitpick comments (1)
source/units/Goccia.Parser.pas (1)

2851-2897: Consider extracting shared declaration parsing logic to reduce drift risk.

VarStatement and DeclarationStatement now duplicate most declarator/destructuring parsing behavior. A shared helper would make future semantics changes safer and easier to keep in sync.

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

In `@source/units/Goccia.Parser.pas` around lines 2851 - 2897, The code duplicates
declarator/destructuring parsing between VarStatement and DeclarationStatement;
extract the shared logic into a single helper (e.g., ParseDeclarators or
ParseDeclarationDeclarators) that handles: detecting destructuring via
Check(gttLeftBracket)/Check(gttLeftBrace) and calling ParsePattern, optional
type via CollectTypeAnnotation, consuming initializer with Consume(gttAssign) or
creating an undefined literal, and consuming the trailing semicolon via
ConsumeSemicolonOrASI; have the helper return a discriminated result (pattern +
initializer + TypeAnnotation or a list of variable records) so callers can
construct TGocciaDestructuringDeclaration or TGocciaVariableDeclaration,
preserving uses of ParsePattern, CollectTypeAnnotation, Consume,
ConsumeSemicolonOrASI, TGocciaDestructuringDeclaration and
TGocciaVariableDeclaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@source/units/Goccia.Compiler.Statements.pas`:
- Around line 2797-2811: CompileDestructuringDeclaration currently assumes new
locals appear in the ACtx.Scope range between LocalCountBefore and
ACtx.Scope.LocalCount, but when hoisting/DeclareVarLocal pre-declared slots this
range can be empty and top-level var destructuring globals are skipped; change
the end of CompileDestructuringDeclaration (after
EmitDestructuring/FreeRegister) to walk the destructuring pattern's leaf
identifiers and for each identifier re-resolve its Local via the same lookup
used in CompileVariableDeclaration, then call
ACtx.Scope.MarkGlobalBacked(LocalIndex) and EmitGlobalDefine(ACtx, Local.Slot,
Local.Name, AStmt.IsConst) for each leaf; reference functions/idents:
CompileDestructuringDeclaration, CollectDestructuringVarBindings,
DeclareVarLocal, DoCompileFunctionBody, EmitDestructuring,
ACtx.Scope.MarkGlobalBacked, EmitGlobalDefine.

---

Nitpick comments:
In `@source/units/Goccia.Parser.pas`:
- Around line 2851-2897: The code duplicates declarator/destructuring parsing
between VarStatement and DeclarationStatement; extract the shared logic into a
single helper (e.g., ParseDeclarators or ParseDeclarationDeclarators) that
handles: detecting destructuring via Check(gttLeftBracket)/Check(gttLeftBrace)
and calling ParsePattern, optional type via CollectTypeAnnotation, consuming
initializer with Consume(gttAssign) or creating an undefined literal, and
consuming the trailing semicolon via ConsumeSemicolonOrASI; have the helper
return a discriminated result (pattern + initializer + TypeAnnotation or a list
of variable records) so callers can construct TGocciaDestructuringDeclaration or
TGocciaVariableDeclaration, preserving uses of ParsePattern,
CollectTypeAnnotation, Consume, ConsumeSemicolonOrASI,
TGocciaDestructuringDeclaration and TGocciaVariableDeclaration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46104987-ffa2-4eee-aafd-89ff6a3c7a55

📥 Commits

Reviewing files that changed from the base of the PR and between 2a704d0 and d6dddf6.

📒 Files selected for processing (10)
  • docs/decision-log.md
  • docs/language.md
  • source/units/Goccia.AST.Expressions.pas
  • source/units/Goccia.AST.Statements.pas
  • source/units/Goccia.Compiler.Statements.pas
  • source/units/Goccia.Engine.pas
  • source/units/Goccia.Evaluator.pas
  • source/units/Goccia.Parser.pas
  • source/units/Goccia.VM.pas
  • source/units/Goccia.Values.FunctionValue.pas
✅ Files skipped from review due to trivial changes (2)
  • source/units/Goccia.VM.pas
  • source/units/Goccia.AST.Expressions.pas
🚧 Files skipped from review as they are similar to previous changes (6)
  • source/units/Goccia.Values.FunctionValue.pas
  • docs/language.md
  • source/units/Goccia.Engine.pas
  • docs/decision-log.md
  • source/units/Goccia.AST.Statements.pas
  • source/units/Goccia.Evaluator.pas

When var destructuring runs at top-level with global backing enabled,
DeclareVarLocal reuses pre-hoisted slots so LocalCountBefore equals
LocalCount — the existing loop does nothing. Add EmitGlobalDefinesForPattern
that walks the pattern tree, resolves each leaf identifier, and emits
MarkGlobalBacked + EmitGlobalDefine per binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@frostney frostney merged commit 3aea8fe into main Apr 20, 2026
10 checks passed
@frostney frostney deleted the t3code/8d1b55e1 branch April 20, 2026 23:32
frostney added a commit that referenced this pull request Apr 21, 2026
Resolve conflict in GocciaREPL.dpr: incorporate --compat-var support
from main (#368) into the CLI.Application-based rewrite. CreateEngine
handles VarEnabled on the engine; added Parser.VarDeclarationsEnabled
in the bytecode path's manual parser creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation internal Refactoring, CI, tooling, cleanup new feature New feature or request spec compliance Mismatch against official JavaScript/TypeScript specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant