Skip to content

Fix class expression super() bytecode crash in decorator wrapping benchmark#56

Merged
frostney merged 2 commits into
mainfrom
cursor/class-wrapping-benchmark-issue-065c
Mar 10, 2026
Merged

Fix class expression super() bytecode crash in decorator wrapping benchmark#56
frostney merged 2 commits into
mainfrom
cursor/class-wrapping-benchmark-issue-065c

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Mar 9, 2026

Fix a fatal error in bytecode mode for class expressions extending dynamic values with super() calls.

The CompileClassExpression incorrectly used AllocateRegister() for the super register, preventing __super__ from being resolved via upvalue capture in constructors, leading to a crash and the "class decorator (wrapping)" benchmark reporting 0 ops/sec.


Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • Tests

    • Added comprehensive test suite for class expressions extending dynamic base classes, covering constructor super() calls, argument spreading, implicit super behavior, method inheritance patterns, and decorator-like wrapping functionality for enhanced validation.
  • Refactor

    • Optimized internal storage mechanism for superclass references in class declarations and expressions to improve memory efficiency without affecting external behavior.

CompileClassExpression used AllocateRegister() for the super class
register, making it invisible to upvalue capture. Constructors in
class expressions that called super() would resolve __super__ to nil,
causing a fatal error at runtime.

Mirror CompileClassDeclaration by using DeclareLocal('__super__', False)
so that nested method scopes can capture the super binding via upvalue.

This fixes the 'class decorator (wrapping)' benchmark producing 0 ops/sec
in bytecode mode.

Co-authored-by: Johannes Stein <frostney@users.noreply.github.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented Mar 9, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

A new test suite validates class expressions extending dynamic bases with various inheritance patterns, including constructor super() calls, spread arguments, implicit super behavior, additional methods, and decorator wrapping. The compiler's superclass reference handling is modified to use a dedicated local variable slot instead of a temporary register.

Changes

Cohort / File(s) Summary
Test Suite for Class Extension
tests/language/classes/class-expression-extends.js
New test file (+99 lines) covering class expression inheritance patterns: explicit super() calls, spread arguments to super, implicit super without constructor, extended methods, and decorator-like constructor wrapping with instanceof and property propagation validation.
Compiler Superclass Reference Handling
units/Goccia.Compiler.Statements.pas
Changed superclass reference storage from a temporary allocated register to a dedicated local variable slot named __super__. Affects both class expression and class declaration paths; subsequent inheritance setup operations (move, get_upvalue/get_global, inherit, set wrapped super) now target the local slot instead of the transient register.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 Classes now extend with grace so fine,
A local __super__ slot so divine,
From registers freed, the inheritance flows,
While tests bloom bright in delightful rows! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The PR title accurately reflects the main fix: addressing a bytecode crash in class expression super() calls that occurs in decorator wrapping scenarios.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/class-wrapping-benchmark-issue-065c

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 9, 2026

Benchmark Results

254 benchmarks (no baseline)

arraybuffer.js — 14 benchmarks
Benchmark Interpreted Bytecode
create ArrayBuffer(0) 391,249 141,135
create ArrayBuffer(64) 396,022 145,321
create ArrayBuffer(1024) 310,378 127,155
create ArrayBuffer(8192) 136,358 82,409
slice full buffer (64 bytes) 477,189 366,010
slice half buffer (512 of 1024 bytes) 403,123 330,022
slice with negative indices 404,345 359,906
slice empty range 461,145 372,050
byteLength access 1,476,392 1,065,450
Symbol.toStringTag access 1,087,835 518,142
ArrayBuffer.isView 692,347 463,968
clone ArrayBuffer(64) 358,411 313,313
clone ArrayBuffer(1024) 277,637 247,764
clone ArrayBuffer inside object 236,109 152,676
arrays.js — 19 benchmarks
Benchmark Interpreted Bytecode
Array.from length 100 13,666 12,941
Array.from 10 elements 218,927 158,294
Array.of 10 elements 292,100 223,508
spread into new array 310,693 533,443
map over 50 elements 25,842 22,238
filter over 50 elements 22,216 21,460
reduce sum 50 elements 25,472 19,541
forEach over 50 elements 21,576 22,988
find in 50 elements 32,890 27,812
sort 20 elements 11,254 3,371
flat nested array 103,183 249,681
flatMap 64,586 182,933
map inside map (5x5) 18,793 65,383
filter inside map (5x10) 13,676 13,313
reduce inside map (5x10) 16,669 13,531
forEach inside forEach (5x10) 14,449 15,084
find inside some (10x10) 11,688 10,635
map+filter chain nested (5x20) 4,628 4,439
reduce flatten (10x5) 31,536 4,599
async-await.js — 6 benchmarks
Benchmark Interpreted Bytecode
single await 313,121 227,621
multiple awaits 142,232 97,628
await non-Promise value 685,894 643,548
await with try/catch 308,474 225,324
await Promise.all 44,206 39,300
nested async function call 158,058 172,582
classes.js — 31 benchmarks
Benchmark Interpreted Bytecode
simple class new 110,845 354,341
class with defaults 88,647 236,484
50 instances via Array.from 5,237 6,286
instance method call 53,857 158,283
static method call 84,141 375,420
single-level inheritance 41,882 145,150
two-level inheritance 35,093 126,398
private field access 53,107 173,057
private methods 56,317 215,594
getter/setter access 58,106 172,614
class decorator (identity) 71,857 52,526
class decorator (wrapping) 41,999 36,191
identity method decorator 52,573 43,035
wrapping method decorator 42,755 39,241
stacked method decorators (x3) 29,915 27,457
identity field decorator 56,454 45,123
field initializer decorator 47,465 39,672
getter decorator (identity) 49,221 39,885
setter decorator (identity) 40,793 34,817
static method decorator 51,837 61,294
static field decorator 61,238 64,466
private method decorator 39,733 36,557
private field decorator 42,524 37,820
plain auto-accessor (no decorator) 76,906 52,130
auto-accessor with decorator 45,342 36,123
decorator writing metadata 36,165 40,228
static getter read 85,384 387,278
static getter/setter pair 66,113 201,440
inherited static getter 51,398 261,827
inherited static setter 52,946 201,948
inherited static getter with this binding 45,224 151,242
closures.js — 11 benchmarks
Benchmark Interpreted Bytecode
closure over single variable 101,045 580,177
closure over multiple variables 96,169 377,169
nested closures 98,403 526,138
function as argument 78,651 500,551
function returning function 92,488 574,847
compose two functions 57,426 355,333
fn.call 125,750 133,419
fn.apply 92,909 87,636
fn.bind 109,185 144,950
recursive sum to 50 10,310 38,451
recursive tree traversal 16,561 59,296
collections.js — 12 benchmarks
Benchmark Interpreted Bytecode
add 50 elements 6,552 5,708
has lookup (50 elements) 89,340 89,260
delete elements 45,585 38,021
forEach iteration 13,447 15,590
spread to array 22,826 32,091
deduplicate array 33,392 32,713
set 50 entries 4,849 5,919
get lookup (50 entries) 86,799 102,624
has check 127,204 161,776
delete entries 45,632 36,410
forEach iteration 12,977 15,470
keys/values/entries 6,244 8,398
destructuring.js — 22 benchmarks
Benchmark Interpreted Bytecode
simple array destructuring 319,054 616,186
with rest element 215,385 461,135
with defaults 330,960 645,161
skip elements 343,160 680,457
nested array destructuring 132,676 320,900
swap variables 385,467 956,979
simple object destructuring 251,367 519,824
with defaults 296,387 324,395
with renaming 262,770 574,050
nested object destructuring 119,621 248,817
rest properties 156,569 225,022
object parameter 75,112 182,063
array parameter 97,055 311,549
mixed destructuring in map 29,830 34,569
forEach with array destructuring 50,412 129,646
map with array destructuring 50,191 154,364
filter with array destructuring 53,776 179,052
reduce with array destructuring 55,851 173,805
map with object destructuring 62,723 72,981
map with nested destructuring 53,592 60,313
map with rest in destructuring 28,470 47,538
map with defaults in destructuring 48,980 39,231
fibonacci.js — 8 benchmarks
Benchmark Interpreted Bytecode
recursive fib(15) 268 1,136
recursive fib(20) 26 103
recursive fib(15) typed 274 1,441
recursive fib(20) typed 26 131
iterative fib(20) via reduce 9,550 17,155
iterator fib(20) 7,618 11,107
iterator fib(20) via Iterator.from + take 7,209 10,635
iterator fib(20) last value via reduce 6,192 8,995
for-of.js — 7 benchmarks
Benchmark Interpreted Bytecode
for...of with 10-element array 37,914 59,233
for...of with 100-element array 4,279 7,488
for...of with string (10 chars) 27,435 54,900
for...of with Set (10 elements) 38,575 60,816
for...of with Map entries (10 entries) 22,604 11,981
for...of with destructuring 31,867 51,814
for-await-of with sync array 36,051 47,837
iterators.js — 20 benchmarks
Benchmark Interpreted Bytecode
Iterator.from({next}).toArray() — 20 elements 8,735 12,088
Iterator.from({next}).toArray() — 50 elements 3,808 5,073
spread pre-wrapped iterator — 20 elements 8,796 11,646
Iterator.from({next}).forEach — 50 elements 2,940 4,117
Iterator.from({next}).reduce — 50 elements 2,951 3,873
wrap array iterator 45,441 57,451
wrap plain {next()} object 6,198 9,520
map + toArray (50 elements) 2,500 3,435
filter + toArray (50 elements) 2,657 3,807
take(10) + toArray (50 element source) 13,469 18,871
drop(40) + toArray (50 element source) 3,720 5,165
chained map + filter + take (100 element source) 4,341 5,823
some + every (50 elements) 1,706 2,477
find (50 elements) 3,661 5,193
array.values().map().filter().toArray() 2,997 4,902
array.values().take(5).toArray() 47,927 65,510
array.values().drop(45).toArray() 12,878 25,222
map.entries() chained helpers 4,566 2,671
set.values() chained helpers 7,159 10,828
string iterator map + toArray 5,809 9,691
json.js — 20 benchmarks
Benchmark Interpreted Bytecode
parse simple object 141,532 129,121
parse nested object 83,281 82,265
parse array of objects 45,930 49,527
parse large flat object 44,020 47,282
parse mixed types 60,532 67,424
stringify simple object 134,381 130,772
stringify nested object 70,953 69,086
stringify array of objects 37,785 35,732
stringify mixed types 61,870 58,651
reviver doubles numbers 36,915 44,414
reviver filters properties 32,350 45,976
reviver on nested object 41,301 51,837
reviver on array 24,064 29,366
replacer function doubles numbers 37,335 48,601
replacer function excludes properties 48,557 57,572
array replacer (allowlist) 90,807 94,363
stringify with 2-space indent 70,256 68,757
stringify with tab indent 71,134 65,765
parse then stringify 40,493 44,795
stringify then parse 25,047 27,375
jsx.jsx — 21 benchmarks
Benchmark Interpreted Bytecode
simple element 152,018 588,488
self-closing element 158,915 615,628
element with string attribute 130,684 418,268
element with multiple attributes 111,767 365,470
element with expression attribute 119,862 426,230
text child 150,905 598,131
expression child 147,013 533,388
mixed text and expression 146,765 493,598
nested elements (3 levels) 58,821 230,949
sibling children 42,869 174,003
component element 107,059 421,312
component with children 65,153 261,290
dotted component 87,753 314,435
empty fragment 160,094 630,268
fragment with children 44,018 175,065
spread attributes 86,576 105,388
spread with overrides 73,799 78,193
shorthand props 120,978 408,730
nav bar structure 20,220 78,762
card component tree 24,251 85,078
10 list items via Array.from 10,937 23,168
numbers.js — 11 benchmarks
Benchmark Interpreted Bytecode
integer arithmetic 463,697 1,470,733
floating point arithmetic 475,076 1,577,223
number coercion 163,234 133,322
toFixed 87,291 211,930
toString 126,963 654,046
valueOf 169,711 923,397
toPrecision 115,753 393,925
Number.isNaN 258,710 174,425
Number.isFinite 255,398 169,154
Number.isInteger 253,175 186,228
Number.parseInt and parseFloat 212,711 148,943
objects.js — 7 benchmarks
Benchmark Interpreted Bytecode
create simple object 364,512 839,753
create nested object 181,819 372,726
create 50 objects via Array.from 7,911 8,163
property read 521,945 657,552
Object.keys 240,708 203,100
Object.entries 79,134 64,928
spread operator 155,324 210,680
promises.js — 12 benchmarks
Benchmark Interpreted Bytecode
Promise.resolve(value) 393,647 345,574
new Promise(resolve => resolve(value)) 149,807 163,775
Promise.reject(reason) 392,152 330,587
resolve + then (1 handler) 121,133 124,216
resolve + then chain (3 deep) 46,175 56,172
resolve + then chain (10 deep) 14,463 19,087
reject + catch + then 65,049 75,460
resolve + finally + then 53,569 62,550
Promise.all (5 resolved) 20,940 26,596
Promise.race (5 resolved) 22,152 27,980
Promise.allSettled (5 mixed) 17,521 22,798
Promise.any (5 mixed) 20,588 26,121
strings.js — 11 benchmarks
Benchmark Interpreted Bytecode
string concatenation 327,699 400,460
template literal 355,360 679,848
string repeat 326,885 982,727
split and join 110,569 231,492
indexOf and includes 129,128 549,184
toUpperCase and toLowerCase 196,905 685,221
slice and substring 130,428 622,096
trim operations 145,274 720,357
replace and replaceAll 168,056 603,982
startsWith and endsWith 106,453 482,355
padStart and padEnd 171,903 554,562
typed-arrays.js — 22 benchmarks
Benchmark Interpreted Bytecode
new Int32Array(0) 247,611 132,008
new Int32Array(100) 225,183 126,981
new Int32Array(1000) 151,822 73,635
new Float64Array(100) 230,408 128,823
Int32Array.from([...]) 153,109 59,102
Int32Array.of(1, 2, 3, 4, 5) 252,141 247,524
sequential write 100 elements 3,013 6,551
sequential read 100 elements 3,093 5,830
Float64Array write 100 elements 2,949 6,432
fill(42) 46,611 45,552
slice() 188,709 200,528
map(x => x * 2) 6,741 7,271
filter(x => x > 50) 6,965 7,862
reduce (sum) 6,723 6,328
sort() 166,198 156,865
indexOf() 390,151 360,074
reverse() 305,157 273,013
create view over existing buffer 319,119 140,714
subarray() 365,625 366,934
set() from array 497,315 240,472
for-of loop 4,116 8,049
spread into array 13,688 19,134

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

@frostney frostney marked this pull request as ready for review March 10, 2026 07:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

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

1903-1906: ⚠️ Potential issue | 🟠 Major

Remove stale FreeRegister call for SuperReg.

Line 1906 calls FreeRegister for SuperReg, but this is incorrect now that SuperReg is allocated via DeclareLocal (line 1806) rather than AllocateRegister. Locals declared with DeclareLocal are managed by the scope system and freed via EndScope, not FreeRegister.

Compare with CompileClassDeclaration (lines 1757-1760) which does not call FreeRegister for its SuperReg.

🐛 Proposed fix: remove the FreeRegister call
   if HasSuper then
-  begin
-    CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, SuperReg);
-    ACtx.Scope.FreeRegister;
-  end
+    CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, SuperReg)
   else
     CompileDecoratorAndAccessorPass(ACtx, ADest, ClassDef, -1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Compiler.Statements.pas` around lines 1903 - 1906, The call
ACtx.Scope.FreeRegister after CompileDecoratorAndAccessorPass should be removed
because SuperReg is now created via DeclareLocal (not AllocateRegister) and is
managed by the scope system; locate the HasSuper branch in
CompileDecoratorAndAccessorPass/CompileClassDeclaration where SuperReg is used
and delete the ACtx.Scope.FreeRegister invocation so SuperReg is freed via
EndScope like other locals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@units/Goccia.Compiler.Statements.pas`:
- Around line 1903-1906: The call ACtx.Scope.FreeRegister after
CompileDecoratorAndAccessorPass should be removed because SuperReg is now
created via DeclareLocal (not AllocateRegister) and is managed by the scope
system; locate the HasSuper branch in
CompileDecoratorAndAccessorPass/CompileClassDeclaration where SuperReg is used
and delete the ACtx.Scope.FreeRegister invocation so SuperReg is freed via
EndScope like other locals.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44c0e38b-33bb-4588-b008-31b3d23c5043

📥 Commits

Reviewing files that changed from the base of the PR and between 1755a05 and 03a9b3f.

📒 Files selected for processing (2)
  • tests/language/classes/class-expression-extends.js
  • units/Goccia.Compiler.Statements.pas

@frostney frostney changed the title Class wrapping benchmark issue Fix class expression super() bytecode crash in decorator wrapping benchmark Mar 10, 2026
@frostney frostney merged commit d2aea53 into main Mar 10, 2026
7 checks passed
@frostney frostney deleted the cursor/class-wrapping-benchmark-issue-065c branch March 10, 2026 07:42
@frostney frostney added the bug Something isn't working label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants