Skip to content

Add decorators#35

Merged
frostney merged 3 commits into
mainfrom
feat-add-decorators
Feb 23, 2026
Merged

Add decorators#35
frostney merged 3 commits into
mainfrom
feat-add-decorators

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 23, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added full TC39 Stage 3 decorators support for classes, methods, fields, getters/setters, and auto-accessors
    • Added Symbol.metadata for decorator metadata handling
    • Added auto-accessor syntax for class properties
    • Added addInitializer context method for decorators
  • Documentation

    • Updated language reference with decorators and auto-accessor capabilities
    • Updated built-ins documentation to include Symbol.metadata
    • Added decorator benchmarks to performance testing suite
  • Tests

    • Added comprehensive test coverage for decorator features, metadata, and auto-accessors

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

This PR adds comprehensive TC39 Stage 3 decorators support to the Goccia JavaScript evaluator, including parsing, AST representation, decorator evaluation phases, metadata handling, auto-accessor implementation, and extensive test coverage across lexer, parser, evaluator, and runtime components.

Changes

Cohort / File(s) Summary
Documentation & Specifications
AGENTS.md, README.md, docs/benchmarks.md, docs/built-ins.md, docs/language-restrictions.md
Added decorator feature documentation, TC39 references, Symbol.metadata details, and language restriction notes for decorator support and limitations.
Benchmarks
benchmarks/classes.js
Added 206 lines of new decorator-focused benchmark suites covering class, method, field, getter/setter, static, private, auto-accessor, and metadata scenarios with observable runtime execution paths.
Test Suite — Decorators
tests/language/decorators/add-initializer.js, auto-accessor-decorator.js, auto-accessor.js, basic-class-decorator.js, basic-field-decorator.js, basic-getter-setter-decorator.js, basic-method-decorator.js, decorator-context.js, decorator-errors.js, decorator-expressions.js, decorator-metadata.js, decorator-order.js, private-decorators.js, static-decorators.js, symbol-metadata.js
Added 15 new test files (628 lines) covering decorator behavior: context properties, metadata, order, initializers, auto-accessors, private/static elements, and Symbol.metadata identity.
Lexer & Token Infrastructure
units/Goccia.Lexer.pas, units/Goccia.Token.pas
Added new @ token type (gttAt) for decorator syntax recognition in tokenization.
Keywords
units/Goccia.Keywords.Contextual.pas
Added new contextual keyword KEYWORD_ACCESSOR for auto-accessor syntax support.
AST Structures
units/Goccia.AST.Expressions.pas, units/Goccia.AST.Statements.pas
Added TGocciaDecoratorList type, new TGocciaClassElementKind enum, TGocciaClassElement record unifying class member representation, and FDecorators/FElements fields to TGocciaClassDefinition for decorator metadata storage.
Parser
units/Goccia.Parser.pas
Added 196 lines implementing ParseDecoratorExpression, ParseDecorators, and DecoratedClassDeclaration to recognize and apply decorator syntax, integrating decorators into class definition parsing workflow.
Evaluator — Core
units/Goccia.Evaluator.pas
Added 372 lines implementing three-phase decorator evaluation (element decorators, initializers, class decorators), metadata handling, auto-accessor setup, and initialization execution during class definition and instance construction.
Evaluator — Decorator Helpers
units/Goccia.Evaluator.Decorators.pas
Added 96 lines with three helper classes: TGocciaInitializerCollector (manages addInitializer callbacks), TGocciaAccessGetter/TGocciaAccessSetter (property access wrappers for FPC closure workaround).
Values — Symbol Metadata
units/Goccia.Values.SymbolValue.pas, units/Goccia.Builtins.GlobalSymbol.pas
Added WellKnownMetadata class function to TGocciaSymbolValue with lazy initialization and GC pinning; registered Symbol.metadata as global constant on Symbol builtin.
Values — Auto-Accessor
units/Goccia.Values.AutoAccessor.pas
Added 62 lines with TGocciaAutoAccessorGetter and TGocciaAutoAccessorSetter classes for backing field property access in auto-accessors.
Values — Class Decorator Support
units/Goccia.Values.ClassValue.pas
Added 167 lines with new fields and 11 public methods: initializer array management (SetMethodInitializers, SetFieldInitializers, AddFieldInitializer), accessor retrieval (GetGetter, GetSetter, GetStaticGetter, GetStaticSetter), auto-accessor wiring (AddAutoAccessor), and initialization execution (RunMethodInitializers, RunFieldInitializers, RunDecoratorFieldInitializers).

Sequence Diagram

sequenceDiagram
    participant Lexer
    participant Parser
    participant AST as AST/Evaluator
    participant DecEval as Decorator<br/>Evaluator
    participant DecHlpr as Decorator<br/>Helpers
    participant ClassVal as ClassValue

    Lexer->>Parser: Tokenize @ and decorators
    Parser->>Parser: ParseDecorators()
    Parser->>AST: Create class with<br/>FDecorators list
    AST->>DecEval: EvaluateClassDefinition<br/>with decorators
    
    DecEval->>DecEval: Phase 1: Element<br/>decorators
    DecEval->>DecHlpr: Create context for<br/>each element
    DecHlpr->>DecEval: Call decorator<br/>with element & context
    DecEval->>DecEval: Handle return values<br/>(replace/wrap/init)
    
    DecEval->>DecEval: Phase 2: Initializers<br/>(methods/fields)
    DecEval->>DecHlpr: Collect addInitializer<br/>callbacks
    
    DecEval->>DecEval: Phase 3: Class<br/>decorator
    DecEval->>DecEval: Call class decorator<br/>with context
    DecEval->>DecEval: Handle class<br/>transformation
    
    DecEval->>ClassVal: Store initializers &<br/>metadata
    ClassVal->>ClassVal: Attach Symbol.metadata
    
    Note over Lexer,ClassVal: During Instance Construction
    ClassVal->>ClassVal: RunMethodInitializers()
    ClassVal->>ClassVal: RunFieldInitializers()
    ClassVal->>ClassVal: RunDecoratorFieldInitializers()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Type annotations as comments #14: Modifies AST class representation structures (TGocciaClassDefinition) and statement/expression units at the same code level, establishing shared foundation for class metadata.
  • Warning when using unsupported features #15: Introduces contextual keyword infrastructure (Goccia.Keywords.Contextual) and token handling that this PR extends with KEYWORD_ACCESSOR and decorator token support.

Poem

🐰 The rabbit hops through decorator chains,
With metadata that sparkles and sustains,
Auto-accessors back the field so tight,
While context flowers bloom in triple-flight,
From parse to eval, a dance so grand,
TC39 stage three takes a stand!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add decorators' directly and accurately describes the main change in the changeset—implementing TC39 Stage 3 decorators support across the Goccia JavaScript engine.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate 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 feat-add-decorators

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 23, 2026

Benchmark Results

203 benchmarks · 🟢 9 improved · 🔴 64 regressed · 🆕 24 new · 106 unchanged · avg -3.5%

arrays.js — 19 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 14,008 14,226 +1.6%
Array.from 10 elements 164,997 158,982 -3.6%
Array.of 10 elements 201,419 196,824 -2.3%
spread into new array 241,114 238,347 -1.1%
map over 50 elements 11,487 11,544 +0.5%
filter over 50 elements 9,887 9,848 -0.4%
reduce sum 50 elements 12,331 12,391 +0.5%
forEach over 50 elements 9,817 9,871 +0.6%
find in 50 elements 13,166 13,336 +1.3%
sort 20 elements 10,460 10,450 -0.1%
flat nested array 91,968 90,930 -1.1%
flatMap 60,595 59,976 -1.0%
map inside map (5x5) 17,086 16,912 -1.0%
filter inside map (5x10) 5,501 5,323 -3.2%
reduce inside map (5x10) 6,279 6,237 -0.7%
forEach inside forEach (5x10) 5,843 5,701 -2.4%
find inside some (10x10) 3,553 3,446 -3.0%
map+filter chain nested (5x20) 2,375 2,372 -0.1%
reduce flatten (10x5) 6,377 6,288 -1.4%
classes.js — 🔴 5 regressed, 🆕 16 new, 10 unchanged · avg -2.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 102,703 105,691 +2.9%
class with defaults 80,996 81,879 +1.1%
50 instances via Array.from 4,829 4,880 +1.1%
instance method call 48,362 48,538 +0.4%
static method call 83,394 84,082 +0.8%
single-level inheritance 40,893 41,271 +0.9%
two-level inheritance 36,008 36,871 +2.4%
private field access 47,765 48,673 +1.9%
private methods 54,917 56,866 +3.5%
getter/setter access 55,334 54,466 -1.6%
class decorator (identity) 78,029 🆕 new
class decorator (wrapping) 44,955 🆕 new
identity method decorator 55,233 🆕 new
wrapping method decorator 45,421 🆕 new
stacked method decorators (x3) 32,216 🆕 new
identity field decorator 64,624 🆕 new
field initializer decorator 53,654 🆕 new
getter decorator (identity) 52,780 🆕 new
setter decorator (identity) 41,733 🆕 new
static method decorator 55,759 🆕 new
static field decorator 67,454 🆕 new
private method decorator 43,563 🆕 new
private field decorator 47,269 🆕 new
plain auto-accessor (no decorator) 81,451 🆕 new
auto-accessor with decorator 46,400 🆕 new
decorator writing metadata 37,535 🆕 new
static getter read 100,698 87,455 🔴 -13.2%
static getter/setter pair 71,093 62,536 🔴 -12.0%
inherited static getter 56,664 51,381 🔴 -9.3%
inherited static setter 59,923 53,166 🔴 -11.3%
inherited static getter with this binding 48,656 43,395 🔴 -10.8%
closures.js — 🔴 11 regressed · avg -12.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 87,774 79,063 🔴 -9.9%
closure over multiple variables 100,947 88,519 🔴 -12.3%
nested closures 111,971 95,605 🔴 -14.6%
function as argument 79,502 70,320 🔴 -11.5%
function returning function 101,743 88,449 🔴 -13.1%
compose two functions 61,620 53,005 🔴 -14.0%
fn.call 140,410 121,862 🔴 -13.2%
fn.apply 96,293 83,918 🔴 -12.9%
fn.bind 121,464 105,596 🔴 -13.1%
recursive sum to 50 8,252 7,544 🔴 -8.6%
recursive tree traversal 15,032 13,269 🔴 -11.7%
collections.js — 🔴 7 regressed, 5 unchanged · avg -8.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 6,021 5,804 -3.6%
has lookup (50 elements) 9,449 9,164 -3.0%
delete elements 26,841 25,147 -6.3%
forEach iteration 5,920 5,606 -5.3%
spread to array 13,279 12,003 🔴 -9.6%
deduplicate array 42,004 35,559 🔴 -15.3%
set 50 entries 4,601 4,281 -6.9%
get lookup (50 entries) 4,847 4,362 🔴 -10.0%
has check 4,883 4,432 🔴 -9.2%
delete entries 12,957 11,725 🔴 -9.5%
forEach iteration 3,655 3,379 🔴 -7.5%
keys/values/entries 4,736 3,955 🔴 -16.5%
destructuring.js — 🔴 13 regressed, 🆕 8 new, 1 unchanged · avg -10.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 301,324 275,757 🔴 -8.5%
with rest element 190,619 174,318 🔴 -8.6%
with defaults 305,623 276,322 🔴 -9.6%
skip elements 309,954 288,268 -7.0%
nested array destructuring 153,467 134,532 🔴 -12.3%
swap variables 372,845 342,023 🔴 -8.3%
simple object destructuring 251,655 224,411 🔴 -10.8%
with defaults 296,330 272,641 🔴 -8.0%
with renaming 300,231 260,150 🔴 -13.3%
nested object destructuring 148,655 127,416 🔴 -14.3%
rest properties 166,061 149,038 🔴 -10.3%
object parameter 88,310 78,846 🔴 -10.7%
array parameter 105,452 95,513 🔴 -9.4%
mixed destructuring in map 12,112 10,447 🔴 -13.7%
forEach with array destructuring 49,049 🆕 new
map with array destructuring 53,081 🆕 new
filter with array destructuring 52,987 🆕 new
reduce with array destructuring 55,068 🆕 new
map with object destructuring 18,654 🆕 new
map with nested destructuring 14,100 🆕 new
map with rest in destructuring 13,570 🆕 new
map with defaults in destructuring 21,833 🆕 new
fibonacci.js — 🔴 4 regressed, 2 unchanged · avg -8.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 228 225 -1.2%
recursive fib(20) 20 19 -5.3%
iterative fib(20) via reduce 10,291 9,157 🔴 -11.0%
iterator fib(20) 7,493 6,863 🔴 -8.4%
iterator fib(20) via Iterator.from + take 8,481 7,377 🔴 -13.0%
iterator fib(20) last value via reduce 7,195 6,155 🔴 -14.5%
iterators.js — 🔴 18 regressed, 2 unchanged · avg -11.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
Iterator.from({next}).toArray() — 20 elements 9,442 8,276 🔴 -12.3%
Iterator.from({next}).toArray() — 50 elements 4,082 3,611 🔴 -11.5%
spread pre-wrapped iterator — 20 elements 9,470 8,418 🔴 -11.1%
Iterator.from({next}).forEach — 50 elements 3,089 2,773 🔴 -10.2%
Iterator.from({next}).reduce — 50 elements 3,153 2,984 -5.4%
wrap array iterator 62,897 53,163 🔴 -15.5%
wrap plain {next()} object 6,528 5,807 🔴 -11.0%
map + toArray (50 elements) 2,749 2,412 🔴 -12.2%
filter + toArray (50 elements) 2,809 2,482 🔴 -11.6%
take(10) + toArray (50 element source) 15,457 13,211 🔴 -14.5%
drop(40) + toArray (50 element source) 3,888 3,413 🔴 -12.2%
chained map + filter + take (100 element source) 4,942 4,396 🔴 -11.1%
some + every (50 elements) 1,748 1,583 🔴 -9.4%
find (50 elements) 3,843 3,464 🔴 -9.9%
array.values().map().filter().toArray() 3,501 2,909 🔴 -16.9%
array.values().take(5).toArray() 18,878 16,492 🔴 -12.6%
array.values().drop(45).toArray() 11,523 9,699 🔴 -15.8%
map.entries() chained helpers 3,692 3,252 🔴 -11.9%
set.values() chained helpers 6,105 5,704 -6.6%
string iterator map + toArray 8,534 7,179 🔴 -15.9%
json.js — 🔴 6 regressed, 14 unchanged · avg -4.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 174,528 154,086 🔴 -11.7%
parse nested object 113,022 94,744 🔴 -16.2%
parse array of objects 59,203 50,422 🔴 -14.8%
parse large flat object 49,541 45,969 🔴 -7.2%
parse mixed types 74,783 67,265 🔴 -10.1%
stringify simple object 148,848 143,917 -3.3%
stringify nested object 82,809 77,694 -6.2%
stringify array of objects 17,537 15,769 🔴 -10.1%
stringify mixed types 67,187 64,451 -4.1%
reviver doubles numbers 34,170 32,446 -5.0%
reviver filters properties 31,695 31,834 +0.4%
reviver on nested object 42,170 41,723 -1.1%
reviver on array 21,479 21,879 +1.9%
replacer function doubles numbers 35,839 34,394 -4.0%
replacer function excludes properties 45,772 46,059 +0.6%
array replacer (allowlist) 93,493 94,314 +0.9%
stringify with 2-space indent 72,767 76,707 +5.4%
stringify with tab indent 77,628 75,172 -3.2%
parse then stringify 44,918 44,967 +0.1%
stringify then parse 16,962 17,771 +4.8%
jsx.jsx — 21 unchanged · avg +1.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple element 189,434 192,330 +1.5%
self-closing element 196,003 199,787 +1.9%
element with string attribute 161,697 164,980 +2.0%
element with multiple attributes 133,839 134,557 +0.5%
element with expression attribute 145,708 144,018 -1.2%
text child 180,320 191,594 +6.3%
expression child 175,877 178,120 +1.3%
mixed text and expression 165,141 166,562 +0.9%
nested elements (3 levels) 67,816 67,318 -0.7%
sibling children 52,474 50,101 -4.5%
component element 127,757 133,594 +4.6%
component with children 78,737 80,625 +2.4%
dotted component 105,744 109,592 +3.6%
empty fragment 181,874 192,536 +5.9%
fragment with children 50,331 51,382 +2.1%
spread attributes 97,389 99,334 +2.0%
spread with overrides 84,242 86,462 +2.6%
shorthand props 138,044 136,374 -1.2%
nav bar structure 23,819 24,371 +2.3%
card component tree 27,059 27,909 +3.1%
10 list items via Array.from 12,483 12,752 +2.1%
numbers.js — 🟢 1 improved, 10 unchanged · avg +2.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 308,279 343,707 🟢 +11.5%
floating point arithmetic 342,515 365,779 +6.8%
number coercion 158,544 157,802 -0.5%
toFixed 84,467 88,500 +4.8%
toString 126,019 132,907 +5.5%
valueOf 178,172 185,564 +4.1%
toPrecision 116,904 124,721 +6.7%
Number.isNaN 242,478 237,020 -2.3%
Number.isFinite 240,056 229,830 -4.3%
Number.isInteger 216,896 212,526 -2.0%
Number.parseInt and parseFloat 190,197 190,047 -0.1%
objects.js — 7 unchanged · avg -0.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 358,441 353,793 -1.3%
create nested object 182,520 183,169 +0.4%
create 50 objects via Array.from 9,031 8,567 -5.1%
property read 170,473 181,607 +6.5%
Object.keys 120,855 120,173 -0.6%
Object.entries 69,894 69,266 -0.9%
spread operator 151,968 147,446 -3.0%
promises.js — 🟢 5 improved, 7 unchanged · avg +6.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 413,357 411,558 -0.4%
new Promise(resolve => resolve(value)) 155,201 165,602 +6.7%
Promise.reject(reason) 423,202 438,505 +3.6%
resolve + then (1 handler) 130,740 144,152 🟢 +10.3%
resolve + then chain (3 deep) 55,860 58,836 +5.3%
resolve + then chain (10 deep) 18,006 19,007 +5.6%
reject + catch + then 82,079 88,356 🟢 +7.6%
resolve + finally + then 66,761 72,355 🟢 +8.4%
Promise.all (5 resolved) 26,676 28,594 🟢 +7.2%
Promise.race (5 resolved) 28,160 29,833 +5.9%
Promise.allSettled (5 mixed) 22,979 24,796 🟢 +7.9%
Promise.any (5 mixed) 27,267 29,049 +6.5%
strings.js — 🟢 3 improved, 8 unchanged · avg +5.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 356,163 363,650 +2.1%
template literal 379,832 386,234 +1.7%
string repeat 344,080 367,029 +6.7%
split and join 128,442 133,354 +3.8%
indexOf and includes 137,512 146,936 +6.9%
toUpperCase and toLowerCase 203,003 224,806 🟢 +10.7%
slice and substring 116,571 124,110 +6.5%
trim operations 151,131 163,456 🟢 +8.2%
replace and replaceAll 176,578 175,795 -0.4%
startsWith and endsWith 113,252 122,529 🟢 +8.2%
padStart and padEnd 163,182 171,338 +5.0%

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (4)
tests/language/decorators/decorator-errors.js (1)

7-18: Consider using the toThrow() matcher for cleaner error testing.

The test framework supports a toThrow() matcher that would simplify this test and provide better error messages on failure.

♻️ Suggested refactor using toThrow()
-  test("non-callable decorator throws", () => {
-    let threw = false;
-    try {
-      const notAFunction = 42;
-
-      `@notAFunction`
-      class C {}
-    } catch (e) {
-      threw = true;
-    }
-    expect(threw).toBe(true);
+  test("non-callable decorator throws", () => {
+    expect(() => {
+      const notAFunction = 42;
+
+      `@notAFunction`
+      class C {}
+    }).toThrow();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/language/decorators/decorator-errors.js` around lines 7 - 18, Replace
the manual try/catch/flag pattern in the "non-callable decorator throws" test
with Jest's toThrow matcher: wrap the class declaration that uses the
non-callable decorator (the const notAFunction = 42 and the decorated class C)
inside a function passed to expect, and assert .toThrow() on it (e.g., expect(()
=> { const notAFunction = 42; `@notAFunction` class C {} }).toThrow()); remove the
threw variable and try/catch block.
tests/language/decorators/decorator-order.js (1)

7-21: Rename test to match bottom-up application order.

The assertion checks bottom-up application, so the description should reflect that.

♻️ Suggested rename
-  test("decorators evaluated top-to-bottom", () => {
+  test("decorators applied bottom-up", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/language/decorators/decorator-order.js` around lines 7 - 21, Rename the
test description string in the test(...) declaration so it accurately describes
bottom-up application order (e.g., change "decorators evaluated top-to-bottom"
to "decorators applied bottom-to-top"), leaving the test body, the order array
and class C unchanged; update the test(...) call's first argument near the class
C definition and the order/assertions to reflect the new descriptive text.
tests/language/decorators/decorator-metadata.js (1)

1-60: Consider adding metadata inheritance edge-case coverage.

The header mentions prototype-chain inheritance; a small test asserting subclass metadata inherits from the base class would round out edge-case coverage.

As per coding guidelines: “When implementing a new language feature, create JavaScript test files following existing patterns in tests/. Tests should cover happy paths, edge cases, and error cases. Verify all tests pass before committing by running: ./build.pas testrunner && ./build/TestRunner tests”.

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

In `@tests/language/decorators/decorator-metadata.js` around lines 1 - 60, Add a
new test case that covers the prototype-chain inheritance behavior by creating a
base class with a class-level decorator (e.g., reuse the existing track/first
decorator pattern that writes to context.metadata) and then declare a subclass
that extends the base; assert that the subclass’s Symbol.metadata either
contains the inherited metadata entries from the base (or that the subclass
metadata object is linked appropriately per spec), and also verify that
modifying subclass metadata does not unexpectedly mutate the base metadata
object; reference the existing test patterns using class C, context.metadata and
Symbol.metadata to match style and placement in the same file.
units/Goccia.Evaluator.Decorators.pas (1)

45-53: Consider pre-allocating or batching array growth.

Calling SetLength for each added initializer results in O(n²) reallocation cost. For classes with many decorated elements, this may impact performance.

♻️ Alternative using capacity doubling
+private
+  FInitializers: array of TGocciaValue;
+  FCount: Integer;
+public
+  constructor Create;
+  ...
+
+constructor TGocciaInitializerCollector.Create;
+begin
+  FCount := 0;
+  SetLength(FInitializers, 4); // Initial capacity
+end;
+
 function TGocciaInitializerCollector.AddInitializer(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 begin
   if (AArgs.Length > 0) and AArgs.GetElement(0).IsCallable then
   begin
-    SetLength(FInitializers, Length(FInitializers) + 1);
-    FInitializers[High(FInitializers)] := AArgs.GetElement(0);
+    if FCount >= Length(FInitializers) then
+      SetLength(FInitializers, Length(FInitializers) * 2);
+    FInitializers[FCount] := AArgs.GetElement(0);
+    Inc(FCount);
   end;
   Result := TGocciaUndefinedLiteralValue.UndefinedValue;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Evaluator.Decorators.pas` around lines 45 - 53, The
AddInitializer method grows FInitializers by one each call causing quadratic
reallocations; modify TGocciaInitializerCollector to track count and capacity
(e.g., FInitializersCount and FInitializersCapacity) and change AddInitializer
to check capacity before adding: when capacity is zero or full, grow capacity
(e.g., double it or add a fixed chunk) with a single SetLength(FInitializers,
FInitializersCapacity), then append the new initializer to
FInitializers[FInitializersCount] and increment FInitializersCount; keep Result
logic the same. Ensure any existing code that reads FInitializers uses the
actual count (FInitializersCount) or resizes the array down when finalizing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/language/decorators/basic-field-decorator.js`:
- Around line 6-56: Add explicit edge/error-case tests to
tests/language/decorators/basic-field-decorator.js: add test cases that assert
(1) a decorator that returns a non-callable value throws a TypeError when
applied to a field (reference the existing noop/doubleInit patterns and class
C), (2) a decorator that throws synchronously causes class definition or
instantiation to propagate the error (use a decorator function that throws and
expect the error), and (3) invalid context usages (e.g., asserting
context.kind/name/static/private values for static/private fields) by defining
classes with static and private fields and verifying the decorator’s received
context; follow the existing test style (describe/test/expect) and ensure each
new test uses expect(...).toThrow or expect(...).toBe assertions consistent with
surrounding tests.

In `@tests/language/decorators/basic-method-decorator.js`:
- Around line 30-47: The decorator `double` currently returns an arrow function
which lexically binds `this`, causing `method.call(this)` to use the wrong
`this`; update `double` to return a regular function (e.g., function(...args) {
... }) so `this` is dynamically bound at call time, forward received arguments
to the original `method` (use method.apply or call with args), and return the
doubled result; update references in the diff to the `double` function and
ensure `Calculator.prototype.value` wrapping preserves method semantics.

In `@units/Goccia.Evaluator.Decorators.pas`:
- Around line 89-94: TGocciaAccessSetter.SetValue currently calls
AArgs.GetElement(0) without ensuring an argument exists; add a defensive check
(e.g., if AArgs.Length > 0) before calling AArgs.GetElement(0) and only call
TGocciaObjectValue(AThisValue).AssignProperty(FPropertyName,
AArgs.GetElement(0)) when the check passes; if no argument is available, avoid
calling GetElement(0) (either skip the assignment or handle the error/raise as
appropriate for the codebase) so the method no longer accesses out-of-bounds
arguments.

In `@units/Goccia.Evaluator.pas`:
- Around line 1964-2026: Replace all hardcoded decorator context/access string
literals in this block with the constants from Goccia.Constants.PropertyNames
(and add any missing PropertyNames such as kind, name, static, private,
metadata, access, get, set, init, addInitializer); specifically update calls
like ContextObject.AssignProperty('kind'...),
ContextObject.AssignProperty('name'...),
ContextObject.AssignProperty('metadata'...),
AccessObject.AssignProperty('get'...), AccessObject.AssignProperty('set'...),
and the addInitializer TGocciaNativeFunctionValue.CreateWithoutPrototype
invocations (StaticFieldCollector.AddInitializer, FieldCollector.AddInitializer,
MethodCollector.AddInitializer) to use PropertyNames constants instead of string
literals; keep the existing value constructors (TGocciaStringLiteralValue,
TGocciaBooleanLiteralValue, TGocciaObjectValue, TGocciaNativeFunctionValue) and
helper classes (TGocciaAccessGetter, TGocciaAccessSetter) unchanged, and add any
missing entries to Goccia.Constants.PropertyNames for the names referenced
elsewhere (also apply the same replacements in the other mentioned ranges).
- Around line 1898-1916: Add a spec-section annotation for the TC39 decorators
algorithm by inserting a comment in the required format (e.g. "// TC39
Decorators §X.Y.Z MethodName(params)") either immediately above the decorator
handling block shown (the loop that processes cekAccessor and calls
ClassValue.AddAutoAccessor) or on the surrounding routine (e.g.
EvaluateClassDefinition/whatever function contains this block); ensure the
proposal name "TC39 Decorators" is used and include the section number and
method signature per the coding guideline format.
- Around line 1956-2163: The code checks DecoratorFn.IsCallable and reports
errors but then proceeds to cast/use DecoratorFn and DecoratorResult which can
cause invalid-cast exceptions; after each AContext.OnError where a non-callable
decorator or invalid return is detected (e.g., the IsCallable check on
DecoratorFn, the "must return a function or undefined" checks in
cekMethod/cekGetter/cekSetter/cekField, and the "must return an object or
undefined" check in cekAccessor) add an early exit from that decorator handling
(e.g., continue to the next decorator) or raise so you do not perform
casts/Calls on invalid values; specifically guard/use early returns before
calling TGocciaFunctionBase(DecoratorFn).Call and before casting DecoratorResult
to TGocciaFunctionValue, TGocciaMethodValue or TGocciaObjectValue (references:
DecoratorFn, DecoratorArgs, TGocciaFunctionBase.Call, DecoratorResult,
ClassValue.AddPrivateMethod/AddPrivateGetter/AddPrivateSetter,
ClassValue.SetProperty/Prototype.AssignProperty, ClassValue.AddGetter/AddSetter,
ClassValue.AddFieldInitializer, and the accessor handling where DecoratorResult
must be TGocciaObjectValue).
- Around line 1921-2240: The decorator evaluation and application code creates
MetadataObject and collectors (MethodCollector, FieldCollector,
StaticFieldCollector, ClassCollector) but only frees/removes them at the end;
wrap the entire decorator phases (after MetadataObject creation through the
final static initializer loop) in a try..finally so
TGocciaGarbageCollector.Instance.RemoveTempRoot(MetadataObject) and Free on
MethodCollector, FieldCollector, StaticFieldCollector and ClassCollector always
run even if a decorator throws; ensure any temp objects created for each
decorator (e.g., ContextObject, AccessObject, DecoratorArgs) are still freed as
before but that the collectors and MetadataObject cleanup is moved into the
finally block.
- Around line 1898-1913: The initializer is being registered on the public
accessor name (Elem.Name) instead of the backing field, causing the instance
initializer to miss the backing storage used by the getter/setter; change the
call that registers the initializer so AddInstanceProperty is invoked with
AccessorBackingName (not Elem.Name) when Elem.FieldInitializer is assigned,
keeping the AddAutoAccessor(Elem.Name, AccessorBackingName, Elem.IsStatic) call
as-is so the accessor and backing-field initializer align (refer to
AClassDef.FElements loop, Elem.FieldInitializer, AccessorBackingName,
AddInstanceProperty and AddAutoAccessor).
- Around line 2190-2195: The code currently validates DecoratorResult with
IsCallable but then unsafely casts it to TGocciaClassValue
(TGocciaClassValue(DecoratorResult)), which will crash for non-class callables;
change this to accept any callable per TC39 by removing the direct cast and
keeping DecoratorResult as a generic TGocciaValue (or a new local TGocciaValue
variable) and update subsequent logic that used ClassValue to perform runtime
checks (e.g., "if DecoratorResult is TGocciaClassValue then" before calling
class-specific methods) or treat callables uniformly; ensure error reporting via
AContext.OnError remains correct and only reject non-callables (still treat
TGocciaUndefinedLiteralValue as before).

In `@units/Goccia.Values.AutoAccessor.pas`:
- Around line 55-60: The SetValue implementation in TGocciaAutoAccessorSetter
currently uses AArgs.GetElement(0) without verifying there is at least one
argument; update TGocciaAutoAccessorSetter.SetValue to check the argument count
(e.g. AArgs.Length or Count) before calling AArgs.GetElement(0), only calling
TGocciaObjectValue(AThisValue).AssignProperty(FBackingName, AArgs.GetElement(0))
when an argument exists, and otherwise return
TGocciaUndefinedLiteralValue.UndefinedValue (or handle the empty-argument case
consistently with other setters).

In `@units/Goccia.Values.ClassValue.pas`:
- Around line 44-51: MarkReferences currently doesn't mark the new TGocciaValue
initializer arrays so those values can be collected; update the class's
MarkReferences method to iterate and call Mark on each TGocciaValue in
FMethodInitializers and FFieldInitializers and also iterate
FDecoratorFieldInitializers calling Mark on each record's Initializer
(respecting IsPrivate/IsStatic only if relevant), ensuring all TGocciaValue
references are marked; locate the MarkReferences implementation and add loops
that call the existing MarkReferences/Mark helper on each TGocciaValue element
to prevent GC collection.
- Around line 551-565: TGocciaClassValue.RunMethodInitializers currently
unsafely casts entries of FMethodInitializers to TGocciaFunctionBase; modify the
loop to verify the type before calling by checking each FMethodInitializers[Idx]
with the "is" operator (or appropriate type-check) and only create
TGocciaArgumentsCollection and call TGocciaFunctionBase(Call) when the item is a
TGocciaFunctionBase, otherwise skip (or log/raise a controlled error) to avoid
runtime exceptions when non-callable values are present.

In `@units/Goccia.Values.SymbolValue.pas`:
- Around line 183-193: Add a spec-style comment above the
TGocciaSymbolValue.WellKnownMetadata function using the required format for TC39
proposals, e.g. "// TC39 proposal-decorator-metadata §X.Y.Z WellKnownMetadata",
so the method implementing the proposal is annotated; locate the function
(TGocciaSymbolValue.WellKnownMetadata) that initializes FWellKnownMetadata and
pins it via TGocciaGarbageCollector.Instance.PinValue and insert the single-line
comment immediately above the function declaration following the exact "// TC39
<Proposal> §X.Y.Z MethodName" pattern.

---

Nitpick comments:
In `@tests/language/decorators/decorator-errors.js`:
- Around line 7-18: Replace the manual try/catch/flag pattern in the
"non-callable decorator throws" test with Jest's toThrow matcher: wrap the class
declaration that uses the non-callable decorator (the const notAFunction = 42
and the decorated class C) inside a function passed to expect, and assert
.toThrow() on it (e.g., expect(() => { const notAFunction = 42; `@notAFunction`
class C {} }).toThrow()); remove the threw variable and try/catch block.

In `@tests/language/decorators/decorator-metadata.js`:
- Around line 1-60: Add a new test case that covers the prototype-chain
inheritance behavior by creating a base class with a class-level decorator
(e.g., reuse the existing track/first decorator pattern that writes to
context.metadata) and then declare a subclass that extends the base; assert that
the subclass’s Symbol.metadata either contains the inherited metadata entries
from the base (or that the subclass metadata object is linked appropriately per
spec), and also verify that modifying subclass metadata does not unexpectedly
mutate the base metadata object; reference the existing test patterns using
class C, context.metadata and Symbol.metadata to match style and placement in
the same file.

In `@tests/language/decorators/decorator-order.js`:
- Around line 7-21: Rename the test description string in the test(...)
declaration so it accurately describes bottom-up application order (e.g., change
"decorators evaluated top-to-bottom" to "decorators applied bottom-to-top"),
leaving the test body, the order array and class C unchanged; update the
test(...) call's first argument near the class C definition and the
order/assertions to reflect the new descriptive text.

In `@units/Goccia.Evaluator.Decorators.pas`:
- Around line 45-53: The AddInitializer method grows FInitializers by one each
call causing quadratic reallocations; modify TGocciaInitializerCollector to
track count and capacity (e.g., FInitializersCount and FInitializersCapacity)
and change AddInitializer to check capacity before adding: when capacity is zero
or full, grow capacity (e.g., double it or add a fixed chunk) with a single
SetLength(FInitializers, FInitializersCapacity), then append the new initializer
to FInitializers[FInitializersCount] and increment FInitializersCount; keep
Result logic the same. Ensure any existing code that reads FInitializers uses
the actual count (FInitializersCount) or resizes the array down when finalizing.

Comment on lines +6 to +56
describe("field decorators", () => {
test("decorator receives undefined and context", () => {
let receivedValue;
let receivedContext;

const log = (value, context) => {
receivedValue = value;
receivedContext = context;
};

class C {
@log
x = 42;
}

expect(receivedValue).toBe(undefined);
expect(receivedContext.kind).toBe("field");
expect(receivedContext.name).toBe("x");
expect(receivedContext.static).toBe(false);
expect(receivedContext.private).toBe(false);
});

test("decorator can return initializer function", () => {
const doubleInit = (value, context) => {
return (initialValue) => {
return initialValue * 2;
};
};

class C {
@doubleInit
x = 21;
}

const c = new C();
expect(c.x).toBe(42);
});

test("decorator returning undefined keeps original initializer", () => {
const noop = (value, context) => {
return undefined;
};

class C {
@noop
x = 42;
}

const c = new C();
expect(c.x).toBe(42);
});
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 | 🟡 Minor

Add edge/error-case coverage for field decorators.

These tests cover the happy path well; consider adding edge/error cases (e.g., decorator returning a non-callable initializer, throwing inside decorator, or invalid context usage) to complete coverage if not already covered elsewhere.

As per coding guidelines: “When implementing a new language feature, create JavaScript test files following existing patterns in tests/. Tests should cover happy paths, edge cases, and error cases. Verify all tests pass before committing by running: ./build.pas testrunner && ./build/TestRunner tests”.

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

In `@tests/language/decorators/basic-field-decorator.js` around lines 6 - 56, Add
explicit edge/error-case tests to
tests/language/decorators/basic-field-decorator.js: add test cases that assert
(1) a decorator that returns a non-callable value throws a TypeError when
applied to a field (reference the existing noop/doubleInit patterns and class
C), (2) a decorator that throws synchronously causes class definition or
instantiation to propagate the error (use a decorator function that throws and
expect the error), and (3) invalid context usages (e.g., asserting
context.kind/name/static/private values for static/private fields) by defining
classes with static and private fields and verifying the decorator’s received
context; follow the existing test style (describe/test/expect) and ensure each
new test uses expect(...).toThrow or expect(...).toBe assertions consistent with
surrounding tests.

Comment on lines +30 to +47
test("decorator can replace method", () => {
const double = (method, context) => {
return (/* ...args */) => {
const result = method.call(this);
return result * 2;
};
};

class Calculator {
@double
value() {
return 21;
}
}

const calc = new Calculator();
expect(calc.value()).toBe(42);
});
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

Incorrect this binding in decorator replacement function.

The double decorator returns an arrow function which captures this from its lexical scope (the module/global context), not from the method invocation. When calc.value() is called, method.call(this) will use the wrong this value.

To correctly wrap the method while preserving this binding, use a regular function expression or method shorthand.

💚 Proposed fix
   test("decorator can replace method", () => {
     const double = (method, context) => {
-      return (/* ...args */) => {
-        const result = method.call(this);
+      return function(/* ...args */) {
+        const result = method.call(this);
         return result * 2;
       };
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("decorator can replace method", () => {
const double = (method, context) => {
return (/* ...args */) => {
const result = method.call(this);
return result * 2;
};
};
class Calculator {
@double
value() {
return 21;
}
}
const calc = new Calculator();
expect(calc.value()).toBe(42);
});
test("decorator can replace method", () => {
const double = (method, context) => {
return function(/* ...args */) {
const result = method.call(this);
return result * 2;
};
};
class Calculator {
`@double`
value() {
return 21;
}
}
const calc = new Calculator();
expect(calc.value()).toBe(42);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/language/decorators/basic-method-decorator.js` around lines 30 - 47,
The decorator `double` currently returns an arrow function which lexically binds
`this`, causing `method.call(this)` to use the wrong `this`; update `double` to
return a regular function (e.g., function(...args) { ... }) so `this` is
dynamically bound at call time, forward received arguments to the original
`method` (use method.apply or call with args), and return the doubled result;
update references in the diff to the `double` function and ensure
`Calculator.prototype.value` wrapping preserves method semantics.

Comment on lines +89 to +94
function TGocciaAccessSetter.SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
begin
if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then
TGocciaObjectValue(AThisValue).AssignProperty(FPropertyName, AArgs.GetElement(0));
Result := TGocciaUndefinedLiteralValue.UndefinedValue;
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 | 🟡 Minor

Missing bounds check before accessing argument.

SetValue accesses AArgs.GetElement(0) without verifying AArgs.Length > 0, similar to the auto-accessor setter issue.

🛡️ Proposed fix
 function TGocciaAccessSetter.SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 begin
-  if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then
+  if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) and (AArgs.Length > 0) then
     TGocciaObjectValue(AThisValue).AssignProperty(FPropertyName, AArgs.GetElement(0));
   Result := TGocciaUndefinedLiteralValue.UndefinedValue;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Evaluator.Decorators.pas` around lines 89 - 94,
TGocciaAccessSetter.SetValue currently calls AArgs.GetElement(0) without
ensuring an argument exists; add a defensive check (e.g., if AArgs.Length > 0)
before calling AArgs.GetElement(0) and only call
TGocciaObjectValue(AThisValue).AssignProperty(FPropertyName,
AArgs.GetElement(0)) when the check passes; if no argument is available, avoid
calling GetElement(0) (either skip the assignment or handle the error/raise as
appropriate for the codebase) so the method no longer accesses out-of-bounds
arguments.

Comment thread units/Goccia.Evaluator.pas Outdated
Comment on lines +1898 to +1916
// TC39 proposal-decorators: Set up auto-accessors (creates backing field + getter + setter)
for I := 0 to High(AClassDef.FElements) do
begin
if AClassDef.FElements[I].Kind = cekAccessor then
begin
Elem := AClassDef.FElements[I];
AccessorBackingName := '__accessor_' + Elem.Name;

if Assigned(Elem.FieldInitializer) then
ClassValue.AddInstanceProperty(Elem.Name, Elem.FieldInitializer)
else
ClassValue.AddInstanceProperty(Elem.Name, nil);

ClassValue.AddAutoAccessor(Elem.Name, AccessorBackingName, Elem.IsStatic);
end;
end;

// TC39 proposal-decorators: Apply decorators to class elements and class itself
HasDecorators := (Length(AClassDef.FDecorators) > 0) or (Length(AClassDef.FElements) > 0);
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 | 🟡 Minor

Add TC39 Decorators spec annotations for this new algorithm.

The TC39 proposal-decorators headers don’t match the required // TC39 Decorators §X.Y.Z MethodName(params) format. Please add the spec-section annotation above the decorator handling block or on EvaluateClassDefinition. As per coding guidelines: When implementing ECMAScript-specified behavior, annotate each function or method with a comment referencing the relevant spec section in format // ESYYYY §X.Y.Z MethodName(params) where YYYY is the current edition year; for TC39 proposals, reference the proposal name instead.

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

In `@units/Goccia.Evaluator.pas` around lines 1898 - 1916, Add a spec-section
annotation for the TC39 decorators algorithm by inserting a comment in the
required format (e.g. "// TC39 Decorators §X.Y.Z MethodName(params)") either
immediately above the decorator handling block shown (the loop that processes
cekAccessor and calls ClassValue.AddAutoAccessor) or on the surrounding routine
(e.g. EvaluateClassDefinition/whatever function contains this block); ensure the
proposal name "TC39 Decorators" is used and include the section number and
method signature per the coding guideline format.

Comment thread units/Goccia.Evaluator.pas Outdated
Comment on lines +1898 to +1913
// TC39 proposal-decorators: Set up auto-accessors (creates backing field + getter + setter)
for I := 0 to High(AClassDef.FElements) do
begin
if AClassDef.FElements[I].Kind = cekAccessor then
begin
Elem := AClassDef.FElements[I];
AccessorBackingName := '__accessor_' + Elem.Name;

if Assigned(Elem.FieldInitializer) then
ClassValue.AddInstanceProperty(Elem.Name, Elem.FieldInitializer)
else
ClassValue.AddInstanceProperty(Elem.Name, nil);

ClassValue.AddAutoAccessor(Elem.Name, AccessorBackingName, Elem.IsStatic);
end;
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

🧩 Analysis chain

🏁 Script executed:

rg -n "AddAutoAccessor" -C 4

Repository: frostney/GocciaScript

Length of output: 2416


🏁 Script executed:

rg -n "__accessor_" -C 4

Repository: frostney/GocciaScript

Length of output: 649


🏁 Script executed:

sed -n '535,600p' units/Goccia.Values.ClassValue.pas | cat -n

Repository: frostney/GocciaScript

Length of output: 2724


🏁 Script executed:

rg -n "procedure AddInstanceProperty" -A 20 units/Goccia.Values.ClassValue.pas

Repository: frostney/GocciaScript

Length of output: 1756


🏁 Script executed:

rg -n "FInstanceProperties" -C 3 | head -80

Repository: frostney/GocciaScript

Length of output: 2700


🏁 Script executed:

rg -n "procedure.*AddInstanceProperty" units/Goccia.Values.ClassValue.pas -A 15

Repository: frostney/GocciaScript

Length of output: 2251


🏁 Script executed:

rg -n "FInstancePropertyDefs" units/Goccia.Values.ClassValue.pas | head -20

Repository: frostney/GocciaScript

Length of output: 447


🏁 Script executed:

rg -n "FInstancePropertyDefs" units/Goccia.Evaluator.pas | head -20

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -n "RunFieldInitializers\|RunDecoratorFieldInitializers\|InstancePropertyDefs\|RunAutoAccessorInitializers" units/Goccia.Evaluator.pas | head -20

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -n "for.*InstancePropertyDefs\|FInstancePropertyDefs" units/Goccia.Values.ClassValue.pas

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -n "Instantiate\|CreateNativeInstance" units/Goccia.Values.ClassValue.pas -A 30 | head -100

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

sed -n '1880,1930p' units/Goccia.Evaluator.pas | cat -n

Repository: frostney/GocciaScript

Length of output: 2658


🏁 Script executed:

rg -n "InstancePropertyDefs\|InitializeInstanceProperties" units/ -l

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -n "for.*InstancePropertyOrder\|InstancePropertyDefs" units/Goccia.Values.ClassValue.pas -B 2 -A 10

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

grep -rn "CreateNativeInstance\|Instantiate" units/Goccia.Values.ClassValue.pas | head -5

Repository: frostney/GocciaScript

Length of output: 620


🏁 Script executed:

rg -n "class TGocciaClassValue" units/Goccia.Values.ClassValue.pas -A 100 | grep -E "procedure|function" | head -20

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -n "function.*Instantiate\|function.*CreateNativeInstance" units/Goccia.Values.ClassValue.pas -A 40 | head -120

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

wc -l units/Goccia.Values.ClassValue.pas

Repository: frostney/GocciaScript

Length of output: 104


🏁 Script executed:

# Find line numbers where Instantiate and CreateNativeInstance are actually implemented
grep -n "^function.*Instantiate\|^function.*CreateNativeInstance" units/Goccia.Values.ClassValue.pas

Repository: frostney/GocciaScript

Length of output: 1031


🏁 Script executed:

sed -n '648,720p' units/Goccia.Values.ClassValue.pas | cat -n

Repository: frostney/GocciaScript

Length of output: 2609


🏁 Script executed:

rg -n "RunFieldInitializers\|RunDecoratorFieldInitializers" units/Goccia.Evaluator.pas

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

rg -rn "RunFieldInitializers\|RunDecoratorFieldInitializers" units/ --include="*.pas"

Repository: frostney/GocciaScript

Length of output: 506


🏁 Script executed:

rg -n "RunFieldInitializers\|RunDecoratorFieldInitializers" units/

Repository: frostney/GocciaScript

Length of output: 47


Initializer registration uses public accessor name, not backing field.

The initializer is registered via AddInstanceProperty(Elem.Name, Elem.FieldInitializer) before the accessor is created. Since AddAutoAccessor registers the getter/setter on the prototype for the same Elem.Name, the initializer and backing field initialization are misaligned. If instance property initializers are applied at construction time, the initializer on Elem.Name will either shadow the accessor or initialize the wrong location. The initializer should be registered on AccessorBackingName instead to match the backing field that the getter/setter manipulate.

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

In `@units/Goccia.Evaluator.pas` around lines 1898 - 1913, The initializer is
being registered on the public accessor name (Elem.Name) instead of the backing
field, causing the instance initializer to miss the backing storage used by the
getter/setter; change the call that registers the initializer so
AddInstanceProperty is invoked with AccessorBackingName (not Elem.Name) when
Elem.FieldInitializer is assigned, keeping the AddAutoAccessor(Elem.Name,
AccessorBackingName, Elem.IsStatic) call as-is so the accessor and backing-field
initializer align (refer to AClassDef.FElements loop, Elem.FieldInitializer,
AccessorBackingName, AddInstanceProperty and AddAutoAccessor).

Comment on lines +2190 to +2195
if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then
begin
if not DecoratorResult.IsCallable then
AContext.OnError('Class decorator must return a class/function or undefined', ALine, AColumn);
ClassValue := TGocciaClassValue(DecoratorResult);
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

🧩 Analysis chain

🌐 Web query:

TC39 decorators proposal class decorator return value specification requirements

💡 Result:

Under the current TC39 Stage 3 decorators proposal (tc39/proposal-decorators), a class decorator:

  • Has the shape type ClassDecorator = (value: Function, context: { kind: "class"; name: string | undefined; addInitializer(...) }) => Function | void. Returning void (i.e., undefined) means “no replacement; keep the original class”. [1]
  • May replace the class only by returning a callable value (e.g., another class, function, or Proxy). If it returns a non-callable value, a runtime error is thrown (TypeError). [1]

Sources: [1] (github.com)

Citations:


🏁 Script executed:

cat -n units/Goccia.Evaluator.pas | sed -n '2180,2210p'

Repository: frostney/GocciaScript

Length of output: 1757


🏁 Script executed:

rg -A 10 -B 10 "DecoratorResult" units/Goccia.Evaluator.pas | head -80

Repository: frostney/GocciaScript

Length of output: 3674


Fix unsafe cast of decorator return value — class decorators per TC39 may return any callable, not just classes.

The code checks IsCallable correctly per the TC39 decorators spec, but then unsafely casts to TGocciaClassValue. Per TC39, class decorators can return any callable (function, class, Proxy, etc.), not just TGocciaClassValue. The direct cast at line 2194 will crash if a decorator returns a non-class callable.

The error message "Class decorator must return a class/function or undefined" already reflects spec compliance (any callable is valid), but the cast contradicts this. Either:

  1. Reject non-class callables (most conservative): Check DecoratorResult is TGocciaClassValue and update the error message to "must return a class" — but this violates TC39 spec.
  2. Accept any callable (spec-compliant): Remove the unsafe cast. Assign directly to a TGocciaValue variable, or refactor code that depends on class-specific methods to handle arbitrary callables.

Current implementation is inconsistent: accepts any callable in validation but requires a class in the cast.

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

In `@units/Goccia.Evaluator.pas` around lines 2190 - 2195, The code currently
validates DecoratorResult with IsCallable but then unsafely casts it to
TGocciaClassValue (TGocciaClassValue(DecoratorResult)), which will crash for
non-class callables; change this to accept any callable per TC39 by removing the
direct cast and keeping DecoratorResult as a generic TGocciaValue (or a new
local TGocciaValue variable) and update subsequent logic that used ClassValue to
perform runtime checks (e.g., "if DecoratorResult is TGocciaClassValue then"
before calling class-specific methods) or treat callables uniformly; ensure
error reporting via AContext.OnError remains correct and only reject
non-callables (still treat TGocciaUndefinedLiteralValue as before).

Comment on lines +55 to +60
function TGocciaAutoAccessorSetter.SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
begin
if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then
TGocciaObjectValue(AThisValue).AssignProperty(FBackingName, AArgs.GetElement(0));
Result := TGocciaUndefinedLiteralValue.UndefinedValue;
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 | 🟡 Minor

Missing bounds check before accessing argument.

SetValue accesses AArgs.GetElement(0) without verifying AArgs.Length > 0. If called with no arguments, this may access an invalid index.

🛡️ Proposed fix to add bounds check
 function TGocciaAutoAccessorSetter.SetValue(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 begin
-  if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) then
+  if Assigned(AThisValue) and (AThisValue is TGocciaObjectValue) and (AArgs.Length > 0) then
     TGocciaObjectValue(AThisValue).AssignProperty(FBackingName, AArgs.GetElement(0));
   Result := TGocciaUndefinedLiteralValue.UndefinedValue;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.AutoAccessor.pas` around lines 55 - 60, The SetValue
implementation in TGocciaAutoAccessorSetter currently uses AArgs.GetElement(0)
without verifying there is at least one argument; update
TGocciaAutoAccessorSetter.SetValue to check the argument count (e.g.
AArgs.Length or Count) before calling AArgs.GetElement(0), only calling
TGocciaObjectValue(AThisValue).AssignProperty(FBackingName, AArgs.GetElement(0))
when an argument exists, and otherwise return
TGocciaUndefinedLiteralValue.UndefinedValue (or handle the empty-argument case
consistently with other setters).

Comment on lines +44 to +51
FMethodInitializers: array of TGocciaValue;
FFieldInitializers: array of TGocciaValue;
FDecoratorFieldInitializers: array of record
Name: string;
Initializer: TGocciaValue;
IsPrivate: Boolean;
IsStatic: Boolean;
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

MarkReferences must mark initializer arrays to prevent GC collection.

The new fields FMethodInitializers, FFieldInitializers, and FDecoratorFieldInitializers hold TGocciaValue references that are not marked in MarkReferences. Per coding guidelines, each value type must override MarkReferences to mark all TGocciaValue references it holds.

🛡️ Proposed fix: Add marking for initializer arrays

Add to the existing MarkReferences method (after line 307):

   // Mark static symbol descriptors
   for SymPair in FStaticSymbolDescriptors do
   begin
     ...
   end;
+
+  // Mark method initializers
+  for I := 0 to High(FMethodInitializers) do
+    if Assigned(FMethodInitializers[I]) then
+      FMethodInitializers[I].MarkReferences;
+
+  // Mark field initializers
+  for I := 0 to High(FFieldInitializers) do
+    if Assigned(FFieldInitializers[I]) then
+      FFieldInitializers[I].MarkReferences;
+
+  // Mark decorator field initializers
+  for I := 0 to High(FDecoratorFieldInitializers) do
+    if Assigned(FDecoratorFieldInitializers[I].Initializer) then
+      FDecoratorFieldInitializers[I].Initializer.MarkReferences;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.ClassValue.pas` around lines 44 - 51, MarkReferences
currently doesn't mark the new TGocciaValue initializer arrays so those values
can be collected; update the class's MarkReferences method to iterate and call
Mark on each TGocciaValue in FMethodInitializers and FFieldInitializers and also
iterate FDecoratorFieldInitializers calling Mark on each record's Initializer
(respecting IsPrivate/IsStatic only if relevant), ensuring all TGocciaValue
references are marked; locate the MarkReferences implementation and add loops
that call the existing MarkReferences/Mark helper on each TGocciaValue element
to prevent GC collection.

Comment on lines +551 to +565
procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue);
var
Idx: Integer;
Args: TGocciaArgumentsCollection;
begin
for Idx := 0 to High(FMethodInitializers) do
begin
Args := TGocciaArgumentsCollection.Create;
try
TGocciaFunctionBase(FMethodInitializers[Idx]).Call(Args, AInstance);
finally
Args.Free;
end;
end;
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 | 🟡 Minor

Unsafe cast to TGocciaFunctionBase without type verification.

RunMethodInitializers casts initializers directly to TGocciaFunctionBase. If a non-callable value is incorrectly added to the array, this will cause a runtime exception.

🛡️ Proposed fix: Add type guard
 procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue);
 var
   Idx: Integer;
   Args: TGocciaArgumentsCollection;
 begin
   for Idx := 0 to High(FMethodInitializers) do
   begin
+    if not (FMethodInitializers[Idx] is TGocciaFunctionBase) then
+      Continue;
     Args := TGocciaArgumentsCollection.Create;
     try
       TGocciaFunctionBase(FMethodInitializers[Idx]).Call(Args, AInstance);
     finally
       Args.Free;
     end;
   end;
 end;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue);
var
Idx: Integer;
Args: TGocciaArgumentsCollection;
begin
for Idx := 0 to High(FMethodInitializers) do
begin
Args := TGocciaArgumentsCollection.Create;
try
TGocciaFunctionBase(FMethodInitializers[Idx]).Call(Args, AInstance);
finally
Args.Free;
end;
end;
end;
procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue);
var
Idx: Integer;
Args: TGocciaArgumentsCollection;
begin
for Idx := 0 to High(FMethodInitializers) do
begin
if not FMethodInitializers[Idx].IsCallable then
Continue;
Args := TGocciaArgumentsCollection.Create;
try
TGocciaFunctionBase(FMethodInitializers[Idx]).Call(Args, AInstance);
finally
Args.Free;
end;
end;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.ClassValue.pas` around lines 551 - 565,
TGocciaClassValue.RunMethodInitializers currently unsafely casts entries of
FMethodInitializers to TGocciaFunctionBase; modify the loop to verify the type
before calling by checking each FMethodInitializers[Idx] with the "is" operator
(or appropriate type-check) and only create TGocciaArgumentsCollection and call
TGocciaFunctionBase(Call) when the item is a TGocciaFunctionBase, otherwise skip
(or log/raise a controlled error) to avoid runtime exceptions when non-callable
values are present.

Comment thread units/Goccia.Values.SymbolValue.pas Outdated
Comment on lines +183 to +193
// TC39 proposal-decorator-metadata
class function TGocciaSymbolValue.WellKnownMetadata: TGocciaSymbolValue;
begin
if not Assigned(FWellKnownMetadata) then
begin
FWellKnownMetadata := TGocciaSymbolValue.Create('Symbol.metadata');
if Assigned(TGocciaGarbageCollector.Instance) then
TGocciaGarbageCollector.Instance.PinValue(FWellKnownMetadata);
end;
Result := FWellKnownMetadata;
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 | 🟡 Minor

Add spec-style annotation for WellKnownMetadata.

The current comment doesn’t match the required // TC39 <Proposal> §X.Y.Z MethodName format. Please add the proposal section reference and method name above the function.

As per coding guidelines: “When implementing ECMAScript-specified behavior, annotate each function or method with a comment referencing the relevant spec section in format // ESYYYY §X.Y.Z MethodName(params)… For TC39 proposals not yet merged (Temporal, Iterator Helpers), reference the proposal name instead: // TC39 Temporal §5.5.3 MethodName”.

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

In `@units/Goccia.Values.SymbolValue.pas` around lines 183 - 193, Add a spec-style
comment above the TGocciaSymbolValue.WellKnownMetadata function using the
required format for TC39 proposals, e.g. "// TC39 proposal-decorator-metadata
§X.Y.Z WellKnownMetadata", so the method implementing the proposal is annotated;
locate the function (TGocciaSymbolValue.WellKnownMetadata) that initializes
FWellKnownMetadata and pins it via TGocciaGarbageCollector.Instance.PinValue and
insert the single-line comment immediately above the function declaration
following the exact "// TC39 <Proposal> §X.Y.Z MethodName" pattern.

@frostney frostney merged commit 7f20e4a into main Feb 23, 2026
3 checks passed
@frostney frostney deleted the feat-add-decorators branch February 23, 2026 09:13
@frostney frostney added the new feature New feature or request label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant