Skip to content

Refactor CLIs onto shared application framework#299

Merged
frostney merged 8 commits intomainfrom
t3code/shared-cli-library-1
Apr 14, 2026
Merged

Refactor CLIs onto shared application framework#299
frostney merged 8 commits intomainfrom
t3code/shared-cli-library-1

Conversation

@frostney
Copy link
Copy Markdown
Owner

Summary

  • Refactored ScriptLoader, REPL, TestRunner, and BenchmarkRunner to use shared CLI/application base classes instead of bespoke command-line parsing and lifecycle code.
  • Added reusable CLI infrastructure for options, parsing, help generation, engine setup, and application execution.
  • Updated project docs and AGENTS guidance to reflect the new CLI pipeline and template-method lifecycle.
  • Added parser coverage for the new shared CLI argument handling.

Testing

  • Not run
  • Existing project guidance still applies: ./build.pas testrunner && ./build/TestRunner tests
  • Recommended follow-up for this change set: run the native Pascal suite if the refactor touched evaluator/scope/value behavior: ./build.pas clean tests && for t in build/Goccia.*.Test; do "$t"; done

- Add shared CLI application, option, parser, and help units
- Update ScriptLoader, REPL, BenchmarkRunner, and TestRunner to use them
- Document the new CLI lifecycle and architecture
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduce a CLI framework (application base, CLI subclass, option model, parser, help generator, engine setup) and migrate REPL, ScriptLoader, TestRunner, and BenchmarkRunner from manual ParamStr/globals to TGocciaApplication/TGocciaCLIApplication-driven tools and option-driven lifecycles.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, docs/architecture.md, docs/code-style.md, docs/embedding.md
Add CLI library and application lifecycle docs, CLI option naming conventions, and embedding/app-class guidance.
Application Base
units/Goccia.Application.pas, units/Goccia.CLI.Application.pas
Add TGocciaApplication (Run/HandleError/GlobalBuiltins) and TGocciaCLIApplication (Configure → ParseCommandLine → Validate → InitializeSingletons → ExecuteWithPaths → AfterExecute → ShutdownSingletons), option registration helpers, and engine/backend factories.
Option Model
units/Goccia.CLI.Options.pas
New declarative option system: TGocciaOptionBase, Flag/String/Integer/Repeatable, generic RTTI-backed TGocciaEnumOption<T>, TGocciaOptionList, ConcatOptions, and engine/coverage/profiler option groups and enums.
Parser & Help
units/Goccia.CLI.Parser.pas, units/Goccia.CLI.Help.pas, units/Goccia.CLI.Parser.Test.pas
Add ParseCommandLine (long/short options, --name=value or space, repeatables, unknown-option errors), GenerateHelpText (grouped aligned output), and tests exercising parsing/help/options.
Engine Instrumentation Setup
units/Goccia.CLI.EngineSetup.pas
Helpers to conditionally initialize/shutdown coverage and profiler based on CLI option objects.
Refactored CLI programs (REPL, BenchmarkRunner)
REPL.dpr, BenchmarkRunner.dpr
Replace manual ParamStr parsing with TREPLApp/TBenchmarkRunnerApp subclasses; register structured options; use EngineOptions (Mode/ASI/Timeout/ImportMap/Aliases); run via TGocciaApplication.RunApplication; parameterize backend/progress through method args.
Refactored CLI programs (ScriptLoader, TestRunner)
ScriptLoader.dpr, TestRunner.dpr
Large refactors into TGocciaCLIApplication subclasses: Configure/Validate/ExecuteWithPaths/AfterExecute/HandleError; option-backed fields; JSON/error/reporting changes; remove module-level globals and direct GC init/shutdown.
Parser tests / tooling
units/Goccia.CLI.Parser.Test.pas
New console tests validating option types, enum parsing/validation, repeatables, option-group counts, ConcatOptions ordering, and help text rendering.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant App as TGocciaCLIApplication
    participant Builder as BuildAllOptions
    participant Parser as ParseCommandLine
    participant Options as TGocciaOptionArray
    participant Instrument as Coverage/Profiler
    participant Engine as CreateEngine/CreateBytecodeBackend
    participant Runner as ExecuteWithPaths

    Caller->>App: RunApplication(AppClass, Name)
    activate App
    App->>App: Create / Configure()
    App->>Builder: BuildAllOptions()
    Builder-->>Options: TGocciaOptionArray
    App->>Parser: ParseCommandLine(Options)
    activate Parser
    Parser->>Options: Apply parsed values
    Parser-->>App: Return positional Paths
    deactivate Parser

    alt Help requested
        App->>App: GenerateHelpText(Name, UsageLine, Options)
        App->>Caller: Print help & exit
    else Normal run
        App->>App: Validate()
        App->>Instrument: InitializeCoverageIfEnabled(CoverageOptions)
        App->>Instrument: InitializeProfilerIfEnabled(ProfilerOptions)
        App->>Engine: CreateEngine(...) / CreateBytecodeBackend(...)
        App->>Runner: ExecuteWithPaths(Paths)
        Runner-->>App: Results
        App->>Instrument: ShutdownProfilerIfEnabled(ProfilerOptions)
        App->>Instrument: ShutdownCoverageIfEnabled(CoverageOptions)
    end
    App-->>Caller: Exit code
    deactivate App
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor CLIs onto shared application framework' clearly and concisely describes the main change: consolidating multiple CLI tools onto a unified application framework. It is specific, accurate, and directly reflects the primary objective of the changeset.
Description check ✅ Passed The PR description covers the main changes, lists affected files, explains the refactoring rationale, and provides testing guidance. However, it lacks explicit checkbox confirmation for testing items and does not include a direct issue reference (Closes #...).
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 internal Refactoring, CI, tooling, cleanup labels Apr 13, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
TestRunner.dpr (1)

655-669: ⚠️ Potential issue | 🔴 Critical

Keep AllTestResults rooted until after the final property writes.

The temp roots are removed before the last AssignProperty(...) calls. Those allocations can trigger GC, which leaves AllTestResults / AllFailedTests collectible while this function is still mutating them.

🛡️ Suggested fix
-  if Assigned(GC) then
-  begin
-    GC.RemoveTempRoot(AllTestResults);
-    GC.RemoveTempRoot(AllFailedTests);
-  end;
-
   AllTestResults.AssignProperty('totalTests', TGocciaNumberLiteralValue.Create(AFiles.Count * 1.0));
   AllTestResults.AssignProperty('passed', TGocciaNumberLiteralValue.Create(PassedCount));
   AllTestResults.AssignProperty('failed', TGocciaNumberLiteralValue.Create(FailedCount));
   AllTestResults.AssignProperty('skipped', TGocciaNumberLiteralValue.Create(SkippedCount));
   AllTestResults.AssignProperty('totalRunTests', TGocciaNumberLiteralValue.Create(TotalRunCount));
   AllTestResults.AssignProperty('duration', TGocciaNumberLiteralValue.Create(TotalDuration));
   AllTestResults.AssignProperty('assertions', TGocciaNumberLiteralValue.Create(TotalAssertions));
 
   Result.TestResult := AllTestResults;
+
+  if Assigned(GC) then
+  begin
+    GC.RemoveTempRoot(AllTestResults);
+    GC.RemoveTempRoot(AllFailedTests);
+  end;
As per coding guidelines, "Use AddTempRoot/RemoveTempRoot to protect values held only by Pascal code (not in any GocciaScript scope)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestRunner.dpr` around lines 655 - 669, The temp roots for AllTestResults and
AllFailedTests are removed too early (GC.RemoveTempRoot is called before the
final AssignProperty calls), risking collection during property writes; move the
GC.RemoveTempRoot(AllTestResults) and GC.RemoveTempRoot(AllFailedTests) calls to
after all AssignProperty(...) invocations and after Result.TestResult :=
AllTestResults so the values remain rooted through the final mutations (ensure
the existing AddTempRoot calls still surround this region).
🧹 Nitpick comments (2)
units/Goccia.CLI.Help.pas (1)

43-49: Remove the fixed 32-group ceiling.

This helper writes into a fixed array[0..31], so the 33rd help group will raise a range-check error. Sizing the backing array to Length(AOptions) keeps the function bounded without baking in an arbitrary crash threshold.

♻️ Suggested refactor
 const
-  MAX_GROUPS = 32;
   COLUMN_GAP = 2;
 var
   Buffer: TStringBuffer;
-  Groups: array[0..MAX_GROUPS - 1] of TGroupEntry;
+  Groups: array of TGroupEntry;
   GroupCount: Integer;
   MaxWidth, I, J, GroupIndex: Integer;
   FormattedName, Header, Padding: string;
   Option: TGocciaOptionBase;
 begin
   GroupCount := 0;
   MaxWidth := 0;
+  SetLength(Groups, Length(AOptions));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.CLI.Help.pas` around lines 43 - 49, The code currently declares
a fixed-size Groups array (Groups: array[0..MAX_GROUPS - 1] of TGroupEntry)
which will raise a range-check error for >32 help groups; change Groups to a
dynamic array (e.g., Groups: array of TGroupEntry or TArray<TGroupEntry>) and
call SetLength(Groups, Length(AOptions)) (and initialize GroupCount :=
Length(AOptions) or maintain GroupCount based on Length(Groups)) before
populating it; also remove or ignore the MAX_GROUPS constant and ensure any
loops use Length(Groups) rather than the fixed bound to avoid out-of-range
accesses while keeping Buffer: TStringBuffer and GroupCount references
consistent.
units/Goccia.CLI.Parser.Test.pas (1)

42-66: Add direct ParseCommandLine coverage.

This suite validates option objects and help rendering, but it never calls the parser itself. Regressions in --opt=value, --opt value, short flags, positional-path capture, and unknown-option handling would currently slip through.

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

In `@units/Goccia.CLI.Parser.Test.pas` around lines 42 - 66, Add direct parser
tests by extending TCLIOptionsTests.SetupTests to register new test cases that
call ParseCommandLine; specifically add tests like TestParseOptionEquals,
TestParseOptionSeparated, TestShortFlagParsing, TestPositionalPathCapture, and
TestUnknownOptionHandling which invoke ParseCommandLine with crafted argv arrays
and assert expected Option.Present/Value, positional captures, and proper error
raising (TGocciaParseError) for unknown options; locate the registration area in
TCLIOptionsTests.SetupTests and add Test('...', TestParseOptionEquals) etc., and
implement the corresponding test methods to exercise ParseCommandLine, covering
--opt=value, --opt value, -f short flags, positional paths, and unknown-option
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Around line 111-112: Update the CLI pipeline doc to include the Configure step
(placed before ParseCommandLine) so it matches TGocciaCLIApplication behavior:
change the pipeline line to "Options → Configure → ParseCommandLine → Validate →
InitializeSingletons → ExecuteWithPaths → AfterExecute → ShutdownSingletons" and
ensure the documentation notes that Configure is where option sets are
registered (as TGocciaCLIApplication does).

In `@docs/architecture.md`:
- Around line 77-84: The tool-mapping table in the documentation is not
separated by blank lines and triggers MD058; add a single blank line before the
table markdown block and a single blank line after it so the table is isolated
(the block containing the header row and the rows for REPL, ScriptLoader,
TestRunner, BenchmarkRunner must be surrounded by empty lines).

In `@docs/code-style.md`:
- Around line 271-276: The docs currently state enum prefixes and space-form
behavior as universal rules; update the wording to describe them as
defaults/opt-ins instead: explain that TGocciaEnumOption<T> derives CLI values
by stripping a prefix but the prefix length is configurable (not limited to 2
characters) and that AcceptsSpaceForm enables the `--opt value` form (it is not
automatic for all repeatable options), and add a specific note that some options
(e.g., BenchmarkRunner’s `--format`) explicitly disable the space form;
reference TGocciaEnumOption<T>, AcceptsSpaceForm, and BenchmarkRunner `--format`
in the prose so readers know where exceptions apply.

In `@REPL.dpr`:
- Around line 88-96: ParseCommandLine returns Positionals but the REPL ignores
them; add validation after Positionals := ParseCommandLine(AllOptions) to reject
any unexpected positional arguments by checking Positionals.Count and, if >0,
print a clear error and Exit (same pattern used for Help.Present). Update both
places where Positionals is used (the startup block around IsBytecodeMode and
the other occurrence around lines 270-271) to surface the error and stop startup
instead of silently dropping arguments.

In `@units/Goccia.CLI.Application.pas`:
- Around line 184-190: The current InitializeSingletons only calls
InitializeCoverageIfEnabled when FCoverageOptions.Enabled.Present is true, which
skips initialization when only coverage-format or coverage-output are provided;
update TGocciaCLIApplication.InitializeSingletons to call
InitializeCoverageIfEnabled whenever FCoverageOptions is assigned and any of the
coverage-related flags/options are present (e.g.,
FCoverageOptions.Enabled.Present OR FCoverageOptions.Format.Present OR
FCoverageOptions.Output.Present), ensuring
InitializeCoverageIfEnabled(FCoverageOptions) runs for --coverage-format and
--coverage-output as well as --coverage.

In `@units/Goccia.CLI.Options.pas`:
- Around line 305-308: TGocciaRepeatableOption.FormatForHelp currently always
returns '--' + LongName + ' <value>' which misdocuments options when
AcceptsSpaceForm is false; update FormatForHelp to check the instance's
AcceptsSpaceForm flag and render the help string accordingly (use '--' +
LongName + ' <value>' when AcceptsSpaceForm = True, and use '--' + LongName +
'=<value>' when AcceptsSpaceForm = False) so callers like FGlobalFiles (which
sets AcceptsSpaceForm := False) produce correct help text for repeatable
options.

In `@units/Goccia.CLI.Parser.pas`:
- Around line 92-100: In the space-form branch for repeatable options, avoid
consuming a following token that is itself an option flag (e.g.
"--mode=bytecode"); before Inc(I) / Value := ParamStr(I) check that ParamStr(I)
exists and does not start with '-' (or otherwise look like an option), and if it
does look like an option throw TGocciaParseError.CreateFmt('--%s requires a
value', [Name]) instead of accepting it; update the logic around
TGocciaRepeatableOption, Count, I, ParamStr and the code path that eventually
calls Apply so option-shaped tokens are rejected as values.

---

Outside diff comments:
In `@TestRunner.dpr`:
- Around line 655-669: The temp roots for AllTestResults and AllFailedTests are
removed too early (GC.RemoveTempRoot is called before the final AssignProperty
calls), risking collection during property writes; move the
GC.RemoveTempRoot(AllTestResults) and GC.RemoveTempRoot(AllFailedTests) calls to
after all AssignProperty(...) invocations and after Result.TestResult :=
AllTestResults so the values remain rooted through the final mutations (ensure
the existing AddTempRoot calls still surround this region).

---

Nitpick comments:
In `@units/Goccia.CLI.Help.pas`:
- Around line 43-49: The code currently declares a fixed-size Groups array
(Groups: array[0..MAX_GROUPS - 1] of TGroupEntry) which will raise a range-check
error for >32 help groups; change Groups to a dynamic array (e.g., Groups: array
of TGroupEntry or TArray<TGroupEntry>) and call SetLength(Groups,
Length(AOptions)) (and initialize GroupCount := Length(AOptions) or maintain
GroupCount based on Length(Groups)) before populating it; also remove or ignore
the MAX_GROUPS constant and ensure any loops use Length(Groups) rather than the
fixed bound to avoid out-of-range accesses while keeping Buffer: TStringBuffer
and GroupCount references consistent.

In `@units/Goccia.CLI.Parser.Test.pas`:
- Around line 42-66: Add direct parser tests by extending
TCLIOptionsTests.SetupTests to register new test cases that call
ParseCommandLine; specifically add tests like TestParseOptionEquals,
TestParseOptionSeparated, TestShortFlagParsing, TestPositionalPathCapture, and
TestUnknownOptionHandling which invoke ParseCommandLine with crafted argv arrays
and assert expected Option.Present/Value, positional captures, and proper error
raising (TGocciaParseError) for unknown options; locate the registration area in
TCLIOptionsTests.SetupTests and add Test('...', TestParseOptionEquals) etc., and
implement the corresponding test methods to exercise ParseCommandLine, covering
--opt=value, --opt value, -f short flags, positional paths, and unknown-option
behavior.
🪄 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: eb23f5bf-9dd4-4fd6-bf0e-62ef1ab599bf

📥 Commits

Reviewing files that changed from the base of the PR and between 983138a and 9c55b47.

📒 Files selected for processing (15)
  • AGENTS.md
  • BenchmarkRunner.dpr
  • REPL.dpr
  • ScriptLoader.dpr
  • TestRunner.dpr
  • docs/architecture.md
  • docs/code-style.md
  • docs/embedding.md
  • units/Goccia.Application.pas
  • units/Goccia.CLI.Application.pas
  • units/Goccia.CLI.EngineSetup.pas
  • units/Goccia.CLI.Help.pas
  • units/Goccia.CLI.Options.pas
  • units/Goccia.CLI.Parser.Test.pas
  • units/Goccia.CLI.Parser.pas

Comment thread AGENTS.md Outdated
Comment thread docs/architecture.md
Comment thread docs/code-style.md Outdated
Comment thread REPL.dpr Outdated
Comment thread units/Goccia.CLI.Application.pas
Comment on lines +305 to +308
function TGocciaRepeatableOption.FormatForHelp: string;
begin
Result := '--' + LongName + ' <value>';
end;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make help text reflect AcceptsSpaceForm.

FormatForHelp always renders repeatable options as --name <value>, but some callers disable the space form. For example, ScriptLoader.dpr sets FGlobalFiles.AcceptsSpaceForm := False, so the generated help currently documents a syntax the parser rejects.

✏️ Suggested fix
 function TGocciaRepeatableOption.FormatForHelp: string;
 begin
-  Result := '--' + LongName + ' <value>';
+  if FAcceptsSpaceForm then
+    Result := '--' + LongName + ' <value>'
+  else
+    Result := '--' + LongName + '=<value>';
 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
function TGocciaRepeatableOption.FormatForHelp: string;
begin
Result := '--' + LongName + ' <value>';
end;
function TGocciaRepeatableOption.FormatForHelp: string;
begin
if FAcceptsSpaceForm then
Result := '--' + LongName + ' <value>'
else
Result := '--' + LongName + '=<value>';
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.CLI.Options.pas` around lines 305 - 308,
TGocciaRepeatableOption.FormatForHelp currently always returns '--' + LongName +
' <value>' which misdocuments options when AcceptsSpaceForm is false; update
FormatForHelp to check the instance's AcceptsSpaceForm flag and render the help
string accordingly (use '--' + LongName + ' <value>' when AcceptsSpaceForm =
True, and use '--' + LongName + '=<value>' when AcceptsSpaceForm = False) so
callers like FGlobalFiles (which sets AcceptsSpaceForm := False) produce correct
help text for repeatable options.

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.

AcceptsSpaceForm was removed entirely in a previous commit. All repeatable options now accept space-form, all other value options require =. Help text is accurate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@frostney, understood — thanks for the clarification! Since AcceptsSpaceForm has been removed entirely, all repeatable options consistently accept space-form and the --name <value> help text is accurate. My comment no longer applies.


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: units/Goccia.CLI.Options.pas:303-306
Timestamp: 2026-04-14T06:46:05.407Z
Learning: In frostney/GocciaScript (`units/Goccia.CLI.Options.pas`), `TGocciaRepeatableOption` does NOT have an `AcceptsSpaceForm` property — it was removed. All repeatable options accept space-form syntax (`--name value`), and all other value options require `=` syntax (`--name=value`). The `FormatForHelp` output of `--name <value>` for repeatable options is intentionally correct. Do not flag this as mismatched help text in future reviews.

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


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 272
File: units/Goccia.Values.TextDecoderValue.pas:175-214
Timestamp: 2026-04-11T17:28:36.950Z
Learning: In frostney/GocciaScript, `TGocciaArgumentValidator` only validates argument *counts* (RequireExactly, RequireAtLeast, RequireAtMost). It does NOT provide per-value type inspection (e.g., is-undefined, is-string, is-object). Therefore, in `InitializeNativeFromArguments` overrides (and similar constructor init paths) where all arguments are optional and correctness depends on value/type checks rather than count checks, manual checks (e.g., `OptionsArg is TGocciaUndefinedLiteralValue`, `OptionsArg is TGocciaObjectValue`) are correct and should not be replaced or flagged as needing `TGocciaArgumentValidator`. Do not re-flag this pattern in `units/Goccia.Values.*.pas` constructor/init methods when the validation is inherently about value kinds, not argument counts.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-05T07:58:33.798Z
Learning: Applies to **/*.pas : Use named type aliases for generic list specializations (e.g., TGocciaValueList = TObjectList<TGocciaValue>)

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: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : Format-defined newline semantics (e.g., TOML multiline strings, YAML folding) must be implemented explicitly and not rely on `LineEnding`/`sLineBreak`; add regression coverage with explicit `#13#10` / `\r\n` inputs

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : `./format.pas` auto-fixes uses clause ordering, PascalCase function names, and parameter `A` prefix naming

Learnt from: frostney
Repo: frostney/GocciaScript PR: 53
File: souffle/Souffle.Wasm.Emitter.pas:0-0
Timestamp: 2026-03-08T04:50:21.036Z
Learning: In souffle/Souffle.Wasm.Emitter.pas (FreePascal), a code formatter has a known bug where it incorrectly prepends an `A` prefix to opcode constant names inside single-line method bodies (e.g. `begin EmitByte(OP_FOO); end`), turning `OP_FOO` into `AOP_FOO`. This affects all one-liner EmitXxx methods. When reviewing this file, check that no `AOP_*` identifiers appear; they are always formatter-introduced typos and should be `OP_*`.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : Use `Goccia.Constants.*` split constant units instead of hardcoded string literals for property names, type names, error names, constructor names, and symbol names

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 split constant units for runtime constants instead of hardcoded string literals: `Goccia.Constants.PropertyNames` for property names, `Goccia.Constants.TypeNames` for type names, `Goccia.Constants.ErrorNames` for error names, `Goccia.Constants.ConstructorNames` for constructor names, and `Goccia.Constants.SymbolNames` for symbol names.

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-12T22:35:35.657Z
Learning: Applies to **/*.pas : Use `TObjectList<T>` over `TList<T>` when `T` is a class; define named type aliases for generic specializations used across multiple units (e.g., `TGocciaValueList = TObjectList<TGocciaValue>`)

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: 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 **/*.pas : All Pascal units must include `{$I Goccia.inc}` compiler directive.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Comment thread units/Goccia.CLI.Parser.pas
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 5355 5355
Tests Passed 5314 ✅ 5355 ✅
Tests Skipped 41 0
Tests Test Duration 363.7ms 346.9ms
Tests Lex 98.8ms 70.1ms
Tests Parse 129.7ms 138.6ms
Tests Compile 84.3ms
Tests Execute 380.7ms 382.2ms
Tests Engine Total 609.2ms 675.2ms
Benchmarks Total 364 364
Benchmarks Duration 9.96min 8.13min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

Benchmark Results

364 benchmarks

Interpreted: 🟢 60 improved · 🔴 84 regressed · 220 unchanged · avg -0.3%
Bytecode: 🟢 142 improved · 🔴 72 regressed · 150 unchanged · avg +1.4%

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

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.

- Move ScriptLoader, TestRunner, and BenchmarkRunner onto `TGocciaApplication`
- Let CLI subclasses supply their own usage line and simplify option handling
- Keep garbage collector lifecycle outside the shared app wrapper
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@TestRunner.dpr`:
- Around line 133-140: EscapeJSONString currently only replaces \, ", newline,
carriage return and tab, but must also escape all control characters
U+0000..U+001F (e.g., `#0`, `#8` backspace, `#12` form feed) into their JSON-safe
forms; update EscapeJSONString to iterate over the input string and build the
result character-by-character: first handle backslash and double-quote as
existing, map known single-character escapes (\b, \f, \n, \r, \t) to their short
sequences, and for any character with Ord <= 31 emit a \u00XX hex escape,
leaving other characters unchanged; ensure the function still returns correct
UTF-8/Unicode behavior for non-control characters.
🪄 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: 4c652374-958a-4591-8100-c57728748878

📥 Commits

Reviewing files that changed from the base of the PR and between 9c55b47 and 04e2f94.

📒 Files selected for processing (5)
  • BenchmarkRunner.dpr
  • ScriptLoader.dpr
  • TestRunner.dpr
  • units/Goccia.Application.pas
  • units/Goccia.CLI.Application.pas
✅ Files skipped from review due to trivial changes (2)
  • units/Goccia.Application.pas
  • ScriptLoader.dpr
🚧 Files skipped from review as they are similar to previous changes (1)
  • BenchmarkRunner.dpr

Comment thread TestRunner.dpr
Comment on lines +133 to +140
function EscapeJSONString(const S: string): string;
begin
Result := StringReplace(S, '\', '\\', [rfReplaceAll]);
Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
Result := StringReplace(Result, #10, '\n', [rfReplaceAll]);
Result := StringReplace(Result, #13, '\r', [rfReplaceAll]);
Result := StringReplace(Result, #9, '\t', [rfReplaceAll]);
end;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

JSON escaping is incomplete for control characters.

EscapeJSONString escapes \, ", \n, \r, and \t, but JSON requires all control characters (U+0000–U+001F) to be escaped. Characters like backspace (#8), form feed (#12), or NUL (#0) in test output would produce invalid JSON.

🩹 Proposed fix to escape all control characters
 function EscapeJSONString(const S: string): string;
+var
+  I: Integer;
+  C: Char;
+  SB: string;
 begin
-  Result := StringReplace(S, '\', '\\', [rfReplaceAll]);
-  Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
-  Result := StringReplace(Result, `#10`, '\n', [rfReplaceAll]);
-  Result := StringReplace(Result, `#13`, '\r', [rfReplaceAll]);
-  Result := StringReplace(Result, `#9`, '\t', [rfReplaceAll]);
+  SB := '';
+  for I := 1 to Length(S) do
+  begin
+    C := S[I];
+    case C of
+      '\': SB := SB + '\\';
+      '"': SB := SB + '\"';
+      `#8`:  SB := SB + '\b';
+      `#9`:  SB := SB + '\t';
+      `#10`: SB := SB + '\n';
+      `#12`: SB := SB + '\f';
+      `#13`: SB := SB + '\r';
+      `#0`..#7, `#11`, `#14`..#31:
+        SB := SB + Format('\u%.4x', [Ord(C)]);
+    else
+      SB := SB + C;
+    end;
+  end;
+  Result := SB;
 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
function EscapeJSONString(const S: string): string;
begin
Result := StringReplace(S, '\', '\\', [rfReplaceAll]);
Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
Result := StringReplace(Result, #10, '\n', [rfReplaceAll]);
Result := StringReplace(Result, #13, '\r', [rfReplaceAll]);
Result := StringReplace(Result, #9, '\t', [rfReplaceAll]);
end;
function EscapeJSONString(const S: string): string;
var
I: Integer;
C: Char;
SB: string;
begin
SB := '';
for I := 1 to Length(S) do
begin
C := S[I];
case C of
'\': SB := SB + '\\';
'"': SB := SB + '\"';
`#8`: SB := SB + '\b';
`#9`: SB := SB + '\t';
`#10`: SB := SB + '\n';
`#12`: SB := SB + '\f';
`#13`: SB := SB + '\r';
`#0`..#7, `#11`, `#14`..#31:
SB := SB + Format('\u%.4x', [Ord(C)]);
else
SB := SB + C;
end;
end;
Result := SB;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestRunner.dpr` around lines 133 - 140, EscapeJSONString currently only
replaces \, ", newline, carriage return and tab, but must also escape all
control characters U+0000..U+001F (e.g., `#0`, `#8` backspace, `#12` form feed) into
their JSON-safe forms; update EscapeJSONString to iterate over the input string
and build the result character-by-character: first handle backslash and
double-quote as existing, map known single-character escapes (\b, \f, \n, \r,
\t) to their short sequences, and for any character with Ord <= 31 emit a \u00XX
hex escape, leaving other characters unchanged; ensure the function still
returns correct UTF-8/Unicode behavior for non-control characters.

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.

Pre-existing issue not introduced by this PR — EscapeJSONString exists in 5 copies across the codebase. Tracked for consolidation separately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@frostney, understood — thanks for the context. I'll note this for future reviews and won't re-flag the incomplete control-character escaping in the individual tool copies until the consolidation lands.


✏️ Learnings added
Learnt from: frostney
Repo: frostney/GocciaScript PR: 299
File: TestRunner.dpr:132-139
Timestamp: 2026-04-14T06:46:10.919Z
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.

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


🧠 Learnings used
Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:4349-4353
Timestamp: 2026-03-23T12:38:57.366Z
Learning: Repo frostney/GocciaScript — units/Goccia.Runtime.Operations.pas: JSON string escape decoding now uses CodePointToUTF8 and combines UTF‑16 surrogate pairs (\uD800–\uDBFF with \uDC00–\uDFFF) into a single Unicode code point before UTF‑8 encoding. Prefer this helper over Char(StrToInt(...)) for any future code‑point to UTF‑8 conversions in Pascal units.

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

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

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

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

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : `JSON.rawJSON(text)` must create a frozen, null-prototype `TGocciaRawJSONValue` with `rawJSON` property; during `JSON.stringify`, check for `TGocciaRawJSONValue` and emit raw text verbatim

Learnt from: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Lexer.pas:504-533
Timestamp: 2026-04-11T09:43:56.558Z
Learning: In frostney/GocciaScript (units/Goccia.Lexer.pas), the lexer intentionally throws on malformed escape sequences (\u{, \xG1, etc.) regardless of whether the template literal is tagged or untagged. Supporting cooked=undefined for tagged templates (per TC39 proposal-template-literal-revision / ES2018 Template Literal Revision) requires per-segment escape validity tracking and deferred error handling — this is explicitly deferred to a future PR. The raw string (RawSB) is always correctly captured for valid escapes. Do NOT re-flag missing cooked=undefined / malformed-escape tolerance in ScanTemplate or ProcessEscapeSequence as a bug in the current PR.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : Format-defined newline semantics (e.g., TOML multiline strings, YAML folding) must be implemented explicitly and not rely on `LineEnding`/`sLineBreak`; add regression coverage with explicit `#13#10` / `\r\n` inputs

Learnt from: frostney
Repo: frostney/GocciaScript PR: 278
File: units/Goccia.Parser.pas:1010-1023
Timestamp: 2026-04-12T09:44:48.683Z
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-04-12T22:35:35.657Z
Learning: Applies to units/**/*.Test.pas : Update `units/*.Test.pas` native tests to reflect changes in behavior when modifying AST logic, scope chain, evaluator, or value types

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.Test.pas : When modifying AST logic, scope chain, evaluator, or value types, update the native Pascal tests in `units/*.Test.pas` to reflect any changes in behaviour (e.g. new parameters, changed return semantics). Both JavaScript tests and native Pascal tests must pass.

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

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-08T04:38:51.695Z
Learning: Applies to units/**/*.Test.pas : Native Pascal unit tests in `units/*.Test.pas` must be updated whenever AST logic, scope chain, evaluator, or value types are modified. Both JavaScript tests and native Pascal tests must pass.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Build and run native Pascal test suite `./build.pas clean tests && for t in build/Goccia.*.Test; do "$t"; done` when modifying AST logic, scope chain, evaluator, or value types

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

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-08T04:38:51.695Z
Learning: Applies to **/Goccia.Engine.pas : Built-ins are registered by the engine via `TGocciaGlobalBuiltins` flags in `DefaultGlobals`. The TestRunner adds `ggTestAssertions` for `describe`/`test`/`expect`. The BenchmarkRunner adds `ggBenchmark` for `bench()` with setup/teardown.

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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/BaseMap.pas:22-22
Timestamp: 2026-03-18T08:12:38.347Z
Learning: In frostney/GocciaScript, shared/utility units (e.g., BaseMap.pas, HashMap.pas, OrderedMap.pas, OrderedStringMap.pas, garbage collector, test assertion units) that are used across multiple separate parts of the project or can be extracted as standalone infrastructure should NOT use the `Goccia.*` naming prefix. A separate naming convention (to be defined in a follow-up issue) applies to these units. Do not flag the absence of a `Goccia.*` prefix for such shared/utility units in future reviews.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/BaseMap.pas:22-22
Timestamp: 2026-03-18T08:12:38.347Z
Learning: In frostney/GocciaScript, shared/utility units (e.g., BaseMap.pas, HashMap.pas, OrderedMap.pas, OrderedStringMap.pas, garbage collector, test assertion units) that are used across multiple separate parts of the project or can be extracted as standalone infrastructure should NOT use the `Goccia.*` naming prefix. A separate naming convention (to be defined — tracked in issue `#104`) applies to these units. Do not flag the absence of a `Goccia.*` prefix for such shared/utility units in future reviews.

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

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

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 an issue for this

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

@frostney, I've created the issue for consolidating EscapeJSONString into a shared helper and fixing the incomplete control-character escaping. The link will be in the output above once the script runs. It covers both the consolidation work and the full U+0000–U+001F fix.


🏁 Script executed:

gh issue create \
  --repo frostney/GocciaScript \
  --title "Consolidate EscapeJSONString into shared helper and fix incomplete control-character escaping" \
  --body "## Summary

\`EscapeJSONString\` is currently duplicated across approximately 5 CLI tool source files (e.g., \`TestRunner.dpr\`, \`BenchmarkRunner.dpr\`, and others). Each copy only escapes \`\\\`, \`\"\`, \`\\n\`, \`\\r\`, and \`\\t\`, leaving other C0 control characters (U+0000–U+001F, e.g., NUL \`#0\`, backspace \`#8\`, form feed \`#12\`) unescaped, which produces invalid JSON if those characters appear in test output or error messages.

## Work Items

1. **Consolidate** all per-tool copies of \`EscapeJSONString\` into a single shared helper (e.g., in a \`Goccia.JSON.Utils\` or similar unit).
2. **Fix** the shared helper to escape all control characters U+0000–U+001F:
   - Known short escapes: \`\\b\` (`#8`), \`\\t\` (`#9`), \`\\n\` (`#10`), \`\\f\` (`#12`), \`\\r\` (`#13`)
   - Remaining C0 characters: emit \`\\u00XX\` hex escapes
3. **Update** all call sites (TestRunner, BenchmarkRunner, ScriptLoader, REPL, etc.) to use the shared helper.

## References

- Flagged in PR `#299`: https://github.com/frostney/GocciaScript/pull/299#discussion_r3075918260
- Requested by `@frostney`"

Length of output: 75


@frostney, unfortunately the sandbox environment doesn't have GitHub authentication available, so I'm unable to create the issue automatically. Here are the details you can use to open it directly:


Title: Consolidate EscapeJSONString into shared helper and fix incomplete control-character escaping

Body:

## Summary

`EscapeJSONString` is currently duplicated across approximately 5 CLI tool source files
(e.g., `TestRunner.dpr`, `BenchmarkRunner.dpr`, and others). Each copy only escapes
`\`, `"`, `\n`, `\r`, and `\t`, leaving other C0 control characters (U+0000–U+001F,
e.g., NUL `#0`, backspace `#8`, form feed `#12`) unescaped, which produces invalid JSON
if those characters appear in test output or error messages.

## Work Items

1. **Consolidate** all per-tool copies of `EscapeJSONString` into a single shared helper
   (e.g., in a `Goccia.JSON.Utils` or similar unit).
2. **Fix** the shared helper to escape all control characters U+0000–U+001F:
   - Known short escapes: `\b` (`#8`), `\t` (`#9`), `\n` (`#10`), `\f` (`#12`), `\r` (`#13`)
   - Remaining C0 characters: emit `\u00XX` hex escapes
3. **Update** all call sites (TestRunner, BenchmarkRunner, ScriptLoader, REPL, etc.)
   to use the shared helper.

## References

- Flagged in PR `#299`: https://github.com/frostney/GocciaScript/pull/299#discussion_r3075918260
- Requested by `@frostney`

You can create it at: https://github.com/frostney/GocciaScript/issues/new


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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 107
File: units/Goccia.Runtime.Operations.pas:4349-4353
Timestamp: 2026-03-23T12:38:57.366Z
Learning: Repo frostney/GocciaScript — units/Goccia.Runtime.Operations.pas: JSON string escape decoding now uses CodePointToUTF8 and combines UTF‑16 surrogate pairs (\uD800–\uDBFF with \uDC00–\uDFFF) into a single Unicode code point before UTF‑8 encoding. Prefer this helper over Char(StrToInt(...)) for any future code‑point to UTF‑8 conversions in Pascal units.

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

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

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

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

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to **/*.pas : `JSON.rawJSON(text)` must create a frozen, null-prototype `TGocciaRawJSONValue` with `rawJSON` property; during `JSON.stringify`, check for `TGocciaRawJSONValue` and emit raw text verbatim

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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 271
File: units/Goccia.Builtins.GlobalObject.pas:104-129
Timestamp: 2026-04-11T19:43:06.627Z
Learning: In frostney/GocciaScript (units/Goccia.Builtins.GlobalObject.pas and all other built-in units), `TGarbageCollector.AddTempRoot`/`RemoveTempRoot` use `THashMap<TGCManagedObject, Boolean>` which is **set-membership based, NOT reference-counted**. A single `RemoveTempRoot(Obj)` removes the root for ALL holders of that object, including callers higher up the stack (e.g., a benchmark runner that already called `AddTempRoot(SetupResult)`).

Therefore, `AddTempRoot`/`RemoveTempRoot` guards in built-in method implementations MUST be conditioned on whether the value was freshly boxed by `ToObject` (i.e., was not already a `TGocciaObjectValue`). The correct pattern is:

```pascal
if Assigned(TGarbageCollector.Instance) and
   not (AArgs.GetElement(0) is TGocciaObjectValue) then
  TGarbageCollector.Instance.AddTempRoot(Obj);
try
  // ... work ...
finally
  if Assigned(TGarbageCollector.Instance) and
     not (AArgs.GetElement(0) is TGocciaObjectValue) then
    TGarbageCollector.Instance.RemoveTempRoot(Obj);
end;
```

Never unconditionally call `AddTempRoot`/`RemoveTempRoot` on a value that was passed in as an argument — doing so can strip a temp root that an external caller (e.g., a benchmark/test runner) already holds on the same object, causing use-after-free on the next GC cycle.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/OrderedStringMap.pas:118-156
Timestamp: 2026-03-17T22:18:27.332Z
Learning: In frostney/GocciaScript (units/OrderedStringMap.pas), TOrderedStringMap.Remove is currently only called during bridge-cache teardown (inside Destroy) and never in tight loops between Grow triggers. Tombstone accumulation is therefore not a live performance concern for current usage. A hardening measure — tracking FDeletedCount and triggering Compact/Rehash when tombstones dominate — is deferred to a follow-up issue. Do not re-flag tombstone growth in OrderedStringMap.pas as a blocking issue for this PR or until the follow-up lands.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 278
File: units/Goccia.Parser.pas:1010-1023
Timestamp: 2026-04-12T09:44:48.683Z
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: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Compiler.Expressions.pas:1826-1851
Timestamp: 2026-04-11T09:43:49.758Z
Learning: In frostney/GocciaScript (units/Goccia.Compiler.Expressions.pas), `CompileTaggedTemplate` intentionally builds fresh cooked/raw arrays on every call site execution rather than caching a single frozen template object per call site. The `.raw` property is correctly attached as non-enumerable/non-writable/non-configurable via `EmitDefineNonEnumerableProperty` (which emits `Object.defineProperty(cookedArray, 'raw', {value: rawArray})`). Template object identity/caching (reusing the same frozen object across invocations of the same call site, as required by ES2026 §13.2.8.3) is a known deferred optimization tracked for a follow-up PR that adds `FTemplateObject: TGocciaValue` caching to `TGocciaTaggedTemplateExpression`. Do NOT re-flag the per-call allocation as a blocking correctness issue — most tag functions only read from the template object and do not rely on same-site reference equality.

Learnt from: frostney
Repo: frostney/GocciaScript PR: 267
File: units/Goccia.Evaluator.pas:2927-2946
Timestamp: 2026-04-11T09:43:55.698Z
Learning: In frostney/GocciaScript (units/Goccia.Evaluator.pas, EvaluateTaggedTemplate), template object identity — i.e. GetTemplateObject returning the same frozen template object for repeated evaluations of the same source location (AST node / call site) — is a known ES2026 §13.2.8.3 conformance gap, deferred to a follow-up PR. The fix is to cache the built template object on the TGocciaTaggedTemplateExpression AST node (a FTemplateObject field) and reuse it on subsequent calls instead of rebuilding RawArray and CookedArray each time. Do NOT re-flag the per-call rebuild as a blocking issue until the caching follow-up PR lands.

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T22:35:35.657Z
Learning: Applies to units/**/*.Test.pas : Update `units/*.Test.pas` native tests to reflect changes in behavior when modifying AST logic, scope chain, evaluator, or value types

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-20T22:00:59.812Z
Learning: Applies to units/**/*.Test.pas : When modifying AST logic, scope chain, evaluator, or value types, update the native Pascal tests in `units/*.Test.pas` to reflect any changes in behaviour (e.g. new parameters, changed return semantics). Both JavaScript tests and native Pascal tests must pass.

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

Learnt from: CR
Repo: frostney/GocciaScript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-08T04:38:51.695Z
Learning: Applies to units/**/*.Test.pas : Native Pascal unit tests in `units/*.Test.pas` must be updated whenever AST logic, scope chain, evaluator, or value types are modified. Both JavaScript tests and native Pascal tests must pass.

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

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

Learnt from: frostney
Repo: frostney/GocciaScript PR: 66
File: units/BaseMap.pas:22-22
Timestamp: 2026-03-18T08:12:38.347Z
Learning: In frostney/GocciaScript, shared/utility units (e.g., BaseMap.pas, HashMap.pas, OrderedMap.pas, OrderedStringMap.pas, garbage collector, test assertion units) that are used across multiple separate parts of the project or can be extracted as standalone infrastructure should NOT use the `Goccia.*` naming prefix. A separate naming convention (to be defined in a follow-up issue) applies to these units. Do not flag the absence of a `Goccia.*` prefix for such shared/utility units in future reviews.

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.

Issue is being tracked as #303

- Move engine and backend creation into TGocciaCLIApplication
- Simplify repeatable option parsing and help formatting
- Update docs for value-option and repeatable-option syntax
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

Caution

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

⚠️ Outside diff range comments (2)
TestRunner.dpr (2)

561-575: ⚠️ Potential issue | 🟠 Major

Keep the aggregate result rooted until after the final writes.

AllTestResults / AllFailedTests are unrooted before the last AssignProperty(...) calls and before Result.TestResult is published. Those boxed-number allocations can trigger GC while the aggregate object is still being built.

🩹 Minimal fix
-  if Assigned(GC) then
-  begin
-    GC.RemoveTempRoot(AllTestResults);
-    GC.RemoveTempRoot(AllFailedTests);
-  end;
-
-  AllTestResults.AssignProperty('totalTests', TGocciaNumberLiteralValue.Create(AFiles.Count * 1.0));
-  AllTestResults.AssignProperty('passed', TGocciaNumberLiteralValue.Create(PassedCount));
-  AllTestResults.AssignProperty('failed', TGocciaNumberLiteralValue.Create(FailedCount));
-  AllTestResults.AssignProperty('skipped', TGocciaNumberLiteralValue.Create(SkippedCount));
-  AllTestResults.AssignProperty('totalRunTests', TGocciaNumberLiteralValue.Create(TotalRunCount));
-  AllTestResults.AssignProperty('duration', TGocciaNumberLiteralValue.Create(TotalDuration));
-  AllTestResults.AssignProperty('assertions', TGocciaNumberLiteralValue.Create(TotalAssertions));
-
-  Result.TestResult := AllTestResults;
+  try
+    AllTestResults.AssignProperty('totalTests', TGocciaNumberLiteralValue.Create(AFiles.Count * 1.0));
+    AllTestResults.AssignProperty('passed', TGocciaNumberLiteralValue.Create(PassedCount));
+    AllTestResults.AssignProperty('failed', TGocciaNumberLiteralValue.Create(FailedCount));
+    AllTestResults.AssignProperty('skipped', TGocciaNumberLiteralValue.Create(SkippedCount));
+    AllTestResults.AssignProperty('totalRunTests', TGocciaNumberLiteralValue.Create(TotalRunCount));
+    AllTestResults.AssignProperty('duration', TGocciaNumberLiteralValue.Create(TotalDuration));
+    AllTestResults.AssignProperty('assertions', TGocciaNumberLiteralValue.Create(TotalAssertions));
+
+    Result.TestResult := AllTestResults;
+  finally
+    if Assigned(GC) then
+    begin
+      GC.RemoveTempRoot(AllTestResults);
+      GC.RemoveTempRoot(AllFailedTests);
+    end;
+  end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestRunner.dpr` around lines 561 - 575, The code unroots AllTestResults and
AllFailedTests (calls to GC.RemoveTempRoot) before finishing AssignProperty
calls and before publishing Result.TestResult, risking GC of boxed-number
temporaries; to fix, delay removing the temp roots (i.e. move the
GC.RemoveTempRoot(AllTestResults) and GC.RemoveTempRoot(AllFailedTests) calls)
until after all AssignProperty(...) calls and after setting Result.TestResult,
so the aggregate object remains rooted while building and publishing it.

307-323: ⚠️ Potential issue | 🟠 Major

--silent still leaks test-console output in bytecode mode.

The interpreted path silences BuiltinConsole, but this path only suppresses parser warnings. --mode=bytecode --silent will still print test script output to stdout.

🩹 Minimal fix
 var
   Source: TStringList;
   SourceText: string;
   JSXResult: TGocciaJSXTransformResult;
   SourceMap: TGocciaSourceMap;
   Lexer: TGocciaLexer;
   Tokens: TObjectList<TGocciaToken>;
   Parser: TGocciaParser;
   Warning: TGocciaParserWarning;
   ProgramNode: TGocciaProgram;
   Module: TGocciaBytecodeModule;
   Backend: TGocciaBytecodeBackend;
+  SilentCon: TGocciaTestConsole;
   ScriptResult: TGocciaObjectValue;
   ResultValue: TGocciaValue;
   OrigLine, OrigCol, I: Integer;
   LexStart, LexEnd, ParseEnd, CompileEnd, ExecEnd: Int64;
 begin
   ScriptResult := CreateDefaultScriptResult;
+  SilentCon := nil;
 ...
       Backend := CreateBytecodeBackend(AFileName);
       try
+        if FSilent.Present and Assigned(Backend.Bootstrap) and
+           Assigned(Backend.Bootstrap.BuiltinConsole) then
+        begin
+          SilentCon := TGocciaTestConsole.Create;
+          SilentCon.Silence(Backend.Bootstrap.BuiltinConsole.BuiltinObject);
+        end;
         LexStart := GetNanoseconds;
         Lexer := TGocciaLexer.Create(SourceText, AFileName);
         try
           Tokens := Lexer.ScanTokens;
           LexEnd := GetNanoseconds;
@@
       finally
+        SilentCon.Free;
         Backend.Free;
       end;

Also applies to: 352-383

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

In `@TestRunner.dpr` around lines 307 - 323, The bytecode execution path still
writes test-console output because the bytecode backend/module is not configured
to silence the BuiltinConsole; update the bytecode branch (references: Backend,
TGocciaBytecodeBackend, Module, TGocciaBytecodeModule) to apply the same
suppression used by the interpreted path: when the --silent flag is set, replace
or disable the BuiltinConsole used by the backend/module (e.g. set its
Enabled/Active flag to false or install a noop/null console) before
compiling/executing bytecode so stdout from test scripts is suppressed; mirror
this change in the other bytecode-related block noted (lines 352-383 equivalent)
to ensure both places honor --silent.
♻️ Duplicate comments (1)
TestRunner.dpr (1)

132-139: ⚠️ Potential issue | 🟡 Minor

Escape all JSON control characters.

This still leaves #0..#31 values like backspace and form feed unescaped, so certain failure messages will generate invalid JSON.

🩹 Minimal fix
 function EscapeJSONString(const S: string): string;
+var
+  I: Integer;
+  C: Char;
+  SB: string;
 begin
-  Result := StringReplace(S, '\', '\\', [rfReplaceAll]);
-  Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
-  Result := StringReplace(Result, `#10`, '\n', [rfReplaceAll]);
-  Result := StringReplace(Result, `#13`, '\r', [rfReplaceAll]);
-  Result := StringReplace(Result, `#9`, '\t', [rfReplaceAll]);
+  SB := '';
+  for I := 1 to Length(S) do
+  begin
+    C := S[I];
+    case C of
+      '\': SB := SB + '\\';
+      '"': SB := SB + '\"';
+      `#8`:  SB := SB + '\b';
+      `#9`:  SB := SB + '\t';
+      `#10`: SB := SB + '\n';
+      `#12`: SB := SB + '\f';
+      `#13`: SB := SB + '\r';
+      `#0`..#7, `#11`, `#14`..#31:
+        SB := SB + Format('\u%.4x', [Ord(C)]);
+    else
+      SB := SB + C;
+    end;
+  end;
+  Result := SB;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestRunner.dpr` around lines 132 - 139, The EscapeJSONString function
currently only replaces a few characters and misses control characters in the
range `#0`..#31 which can break JSON; update EscapeJSONString to also escape all
control characters: explicitly map known ones (e.g. `#8` -> \b, `#12` -> \f) and for
any remaining codepoints < 32 emit a \u00XX hex escape, or implement a single
pass that builds Result char-by-char and emits either the known escape sequences
or a \u00HH escape for chars < `#32`; keep the existing handling for backslash and
double-quote and ensure the function name EscapeJSONString is updated in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ScriptLoader.dpr`:
- Around line 724-737: Add a validation that rejects using --emit together with
a single output file when there are multiple input paths or the single input is
a directory: in the same validation area that checks FOutputPath and APaths, add
a check that if FEmit.Present and FOutputPath.Present and ((APaths.Count > 1) or
((APaths.Count = 1) and DirectoryExists(APaths[0]))) then raise
TGocciaParseError with a clear message like '--emit with --output=<file>
supports a single input file or stdin.' This references FEmit, FOutputPath and
APaths and prevents RunSource from overwriting the same output for multi-input
runs.

In `@units/Goccia.CLI.Application.pas`:
- Around line 177-188: CreateEngine and CreateBytecodeBackend start the global
execution timeout (StartExecutionTimeout) inside the factory, which can be set
before the caller's try/finally that calls ClearExecutionTimeout; move the
StartExecutionTimeout call out of TGocciaCLIApplication.CreateEngine and the
analogous CreateBytecodeBackend so that factories only configure the
engine/backend (ASIEnabled, ConfigureModuleResolver, etc.), and instead invoke
StartExecutionTimeout immediately before the corresponding execution site (e.g.,
just before Engine.Execute or Backend.RunModule) inside the same try/finally
that calls ClearExecutionTimeout; keep references to StartExecutionTimeout and
ClearExecutionTimeout and ensure the timeout is started and cleared within the
same scope to avoid leaving a stale global timeout on parse/compile errors.

---

Outside diff comments:
In `@TestRunner.dpr`:
- Around line 561-575: The code unroots AllTestResults and AllFailedTests (calls
to GC.RemoveTempRoot) before finishing AssignProperty calls and before
publishing Result.TestResult, risking GC of boxed-number temporaries; to fix,
delay removing the temp roots (i.e. move the GC.RemoveTempRoot(AllTestResults)
and GC.RemoveTempRoot(AllFailedTests) calls) until after all AssignProperty(...)
calls and after setting Result.TestResult, so the aggregate object remains
rooted while building and publishing it.
- Around line 307-323: The bytecode execution path still writes test-console
output because the bytecode backend/module is not configured to silence the
BuiltinConsole; update the bytecode branch (references: Backend,
TGocciaBytecodeBackend, Module, TGocciaBytecodeModule) to apply the same
suppression used by the interpreted path: when the --silent flag is set, replace
or disable the BuiltinConsole used by the backend/module (e.g. set its
Enabled/Active flag to false or install a noop/null console) before
compiling/executing bytecode so stdout from test scripts is suppressed; mirror
this change in the other bytecode-related block noted (lines 352-383 equivalent)
to ensure both places honor --silent.

---

Duplicate comments:
In `@TestRunner.dpr`:
- Around line 132-139: The EscapeJSONString function currently only replaces a
few characters and misses control characters in the range `#0`..#31 which can
break JSON; update EscapeJSONString to also escape all control characters:
explicitly map known ones (e.g. `#8` -> \b, `#12` -> \f) and for any remaining
codepoints < 32 emit a \u00XX hex escape, or implement a single pass that builds
Result char-by-char and emits either the known escape sequences or a \u00HH
escape for chars < `#32`; keep the existing handling for backslash and
double-quote and ensure the function name EscapeJSONString is updated in place.
🪄 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: ce5dff9a-11b1-42c6-a092-f0b3d040fa39

📥 Commits

Reviewing files that changed from the base of the PR and between 04e2f94 and 4af102c.

📒 Files selected for processing (9)
  • BenchmarkRunner.dpr
  • ScriptLoader.dpr
  • TestRunner.dpr
  • docs/code-style.md
  • units/Goccia.CLI.Application.pas
  • units/Goccia.CLI.EngineSetup.pas
  • units/Goccia.CLI.Help.pas
  • units/Goccia.CLI.Options.pas
  • units/Goccia.CLI.Parser.pas
✅ Files skipped from review due to trivial changes (2)
  • docs/code-style.md
  • units/Goccia.CLI.Options.pas
🚧 Files skipped from review as they are similar to previous changes (3)
  • units/Goccia.CLI.Parser.pas
  • units/Goccia.CLI.EngineSetup.pas
  • units/Goccia.CLI.Help.pas

Comment thread ScriptLoader.dpr
Comment thread units/Goccia.CLI.Application.pas
frostney and others added 2 commits April 14, 2026 07:46
- Fix ScriptLoader/TestRunner exit code: RunApplication returning 0 was
  overwriting ExitCode set internally by per-file error handlers
- Move StartExecutionTimeout from engine factory to execution sites to
  avoid stale timeouts during setup
- Reject option-shaped tokens as repeatable values (--alias --mode)
- Reject unexpected positional args in REPL
- Add Matches() method to TGocciaEnumOption<T> for cleaner comparisons
- Fix docs: CLI pipeline includes Configure, markdown table formatting,
  AcceptsSpaceForm removed from docs

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

Caution

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

⚠️ Outside diff range comments (1)
TestRunner.dpr (1)

307-452: ⚠️ Potential issue | 🟠 Major

Handle EGocciaBytecodeThrow explicitly in bytecode mode.

Backend.RunModule() can raise EGocciaBytecodeThrow, but this method only formats TGocciaThrowValue. In bytecode mode, uncaught JS throws therefore fall through to the generic Exception branch and get reported as a plain fatal error instead of a proper throw detail. ScriptLoader.dpr and BenchmarkRunner.dpr already special-case this path.

Suggested fix
     except
       on E: Exception do
       begin
         if E is TGocciaError then
         begin
           WriteLn(TGocciaError(E).GetDetailedMessage(IsColorTerminal));
           MarkLoadError(ScriptResult, AFileName, TGocciaError(E).GetDetailedMessage);
         end
         else if E is TGocciaThrowValue then
         begin
           WriteLn(FormatThrowDetail(TGocciaThrowValue(E).Value, AFileName, Source, IsColorTerminal));
           MarkLoadError(ScriptResult, AFileName,
             FormatThrowDetail(TGocciaThrowValue(E).Value, AFileName, Source, False));
         end
+        else if E is EGocciaBytecodeThrow then
+        begin
+          WriteLn(FormatThrowDetail(EGocciaBytecodeThrow(E).ThrownValue,
+            AFileName, Source, IsColorTerminal));
+          MarkLoadError(ScriptResult, AFileName,
+            FormatThrowDetail(EGocciaBytecodeThrow(E).ThrownValue,
+              AFileName, Source, False));
+        end
         else
         begin
           WriteLn('Fatal error: ', E.Message);
           MarkLoadError(ScriptResult, AFileName, E.Message);
         end;
         Result := MakeEmptyTestResult(ScriptResult);
       end;
     end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestRunner.dpr` around lines 307 - 452, The bytecode runner needs to
explicitly handle EGocciaBytecodeThrow thrown by Backend.RunModule; inside
TTestRunnerApp.RunGocciaScriptBytecode's exception block add an else-if branch
for EGocciaBytecodeThrow (similar to the TGocciaThrowValue handling in
ScriptLoader.dpr/BenchmarkRunner.dpr) that writes the formatted throw detail
(use FormatThrowDetail(EGocciaBytecodeThrow(E).Value, AFileName, Source,
IsColorTerminal)) and calls MarkLoadError(ScriptResult, AFileName,
FormatThrowDetail(..., AFileName, Source, False)); ensure you reference
EGocciaBytecodeThrow, FormatThrowDetail, ScriptResult, AFileName and Source so
uncaught JS throws in bytecode mode are reported like other throw values.
🧹 Nitpick comments (1)
units/Goccia.CLI.Parser.Test.pas (1)

42-65: Add parser-level cases for the new CLI branches.

These tests stop at Option.Apply and GenerateHelpText; none of them exercise ParseCommandLine itself. That leaves the parser-only rules from this refactor untested, especially repeatable --name value handling and rejection of option-shaped/misplaced positional arguments.

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

In `@units/Goccia.CLI.Parser.Test.pas` around lines 42 - 65, Tests only exercise
Option.Apply and GenerateHelpText but not the parser entrypoint; add unit tests
that call ParseCommandLine directly to cover parser-only branches such as
repeatable option accumulation and rejection of option-shaped positional args.
Specifically, add cases that invoke ParseCommandLine with argv arrays containing
repeated "--name value" pairs and assert the resulting RepeatableOption contains
all values, and add cases with positional arguments that look like options
(e.g., "-x" or "--foo=bar" in positional position) and assert ParseCommandLine
returns an error or raises the expected TGocciaParseError; reference
ParseCommandLine, RepeatableOption, Option.Apply and GenerateHelpText when
locating where to add these tests. Ensure assertions verify both successful
accumulation and proper parse-time rejection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.CLI.Options.pas`:
- Around line 226-229: TGocciaFlagOption.Apply currently accepts any AValue
(including non-empty values) and silently treats flags like `--silent=false` as
present; update TGocciaFlagOption.Apply to reject non-empty AValue and fail
fast: if AValue is not empty, raise a clear command-line/parsing exception
(e.g., EArgumentException or the project’s command-line error type) with a
descriptive message mentioning the flag name, otherwise set FPresent := True.
This change affects TGocciaFlagOption.Apply to ensure ParseCommandLine cannot
pass `--flag=value` through silently.
- Around line 379-384: The variable ArdinalValue in TGocciaEnumOption<T>.Matches
is not initialized before calling Move, so when SizeOf(T) < SizeOf(Integer)
leftover bytes produce garbage; to fix, initialize ArdinalValue to 0 (e.g.,
ArdinalValue := 0) immediately before the Move(AValue, ArdinalValue, SizeOf(T))
call so the remaining bytes are deterministic, then keep the existing comparison
Result := FPresent and (FOrdinal = ArdinalValue).

---

Outside diff comments:
In `@TestRunner.dpr`:
- Around line 307-452: The bytecode runner needs to explicitly handle
EGocciaBytecodeThrow thrown by Backend.RunModule; inside
TTestRunnerApp.RunGocciaScriptBytecode's exception block add an else-if branch
for EGocciaBytecodeThrow (similar to the TGocciaThrowValue handling in
ScriptLoader.dpr/BenchmarkRunner.dpr) that writes the formatted throw detail
(use FormatThrowDetail(EGocciaBytecodeThrow(E).Value, AFileName, Source,
IsColorTerminal)) and calls MarkLoadError(ScriptResult, AFileName,
FormatThrowDetail(..., AFileName, Source, False)); ensure you reference
EGocciaBytecodeThrow, FormatThrowDetail, ScriptResult, AFileName and Source so
uncaught JS throws in bytecode mode are reported like other throw values.

---

Nitpick comments:
In `@units/Goccia.CLI.Parser.Test.pas`:
- Around line 42-65: Tests only exercise Option.Apply and GenerateHelpText but
not the parser entrypoint; add unit tests that call ParseCommandLine directly to
cover parser-only branches such as repeatable option accumulation and rejection
of option-shaped positional args. Specifically, add cases that invoke
ParseCommandLine with argv arrays containing repeated "--name value" pairs and
assert the resulting RepeatableOption contains all values, and add cases with
positional arguments that look like options (e.g., "-x" or "--foo=bar" in
positional position) and assert ParseCommandLine returns an error or raises the
expected TGocciaParseError; reference ParseCommandLine, RepeatableOption,
Option.Apply and GenerateHelpText when locating where to add these tests. Ensure
assertions verify both successful accumulation and proper parse-time rejection
behavior.
🪄 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: 38d1e016-40df-46fe-bd82-fda7e9a41f2a

📥 Commits

Reviewing files that changed from the base of the PR and between 4af102c and c3f9307.

📒 Files selected for processing (11)
  • AGENTS.md
  • BenchmarkRunner.dpr
  • REPL.dpr
  • ScriptLoader.dpr
  • TestRunner.dpr
  • docs/architecture.md
  • docs/code-style.md
  • units/Goccia.CLI.Application.pas
  • units/Goccia.CLI.Options.pas
  • units/Goccia.CLI.Parser.Test.pas
  • units/Goccia.CLI.Parser.pas
✅ Files skipped from review due to trivial changes (2)
  • docs/architecture.md
  • docs/code-style.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • AGENTS.md
  • units/Goccia.CLI.Parser.pas
  • REPL.dpr
  • units/Goccia.CLI.Application.pas

Comment on lines +226 to +229
procedure TGocciaFlagOption.Apply(const AValue: string);
begin
FPresent := True;
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

Reject values passed to flag options.

ParseCommandLine forwards --flag=value straight into Apply, and this implementation silently enables the flag. Inputs like --silent=false or --coverage=0 therefore do the opposite of what the user asked instead of failing fast.

Suggested fix
 procedure TGocciaFlagOption.Apply(const AValue: string);
 begin
+  if AValue <> '' then
+    raise TGocciaParseError.CreateFmt(
+      '--%s does not take a value', [LongName]);
   FPresent := True;
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.CLI.Options.pas` around lines 226 - 229, TGocciaFlagOption.Apply
currently accepts any AValue (including non-empty values) and silently treats
flags like `--silent=false` as present; update TGocciaFlagOption.Apply to reject
non-empty AValue and fail fast: if AValue is not empty, raise a clear
command-line/parsing exception (e.g., EArgumentException or the project’s
command-line error type) with a descriptive message mentioning the flag name,
otherwise set FPresent := True. This change affects TGocciaFlagOption.Apply to
ensure ParseCommandLine cannot pass `--flag=value` through silently.

Comment thread units/Goccia.CLI.Options.pas Outdated
The OrdinalValue local was not zero-initialized before Move copied
SizeOf(T) = 1 byte into it.  The remaining 3 bytes held stack garbage,
causing FOrdinal = OrdinalValue to fail on x64 Linux where the garbage
was non-zero.  This made Matches(emBytecode) return False, writing
"mode":"interpreted" into the JSON even when --mode=bytecode was set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
frostney and others added 2 commits April 14, 2026 18:12
…rary-1

# Conflicts:
#	AGENTS.md
#	docs/code-style.md
Add required blank lines between bold paragraph headers and the
list items that follow them in architecture.md and embedding.md.

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

internal Refactoring, CI, tooling, cleanup new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant