Skip to content

Warning when using unsupported features#15

Merged
frostney merged 9 commits into
mainfrom
feat-warning-when-using-unused-features
Feb 19, 2026
Merged

Warning when using unsupported features#15
frostney merged 9 commits into
mainfrom
feat-warning-when-using-unused-features

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 19, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added globalThis global object that references all current bindings
    • Added GocciaScript global object exposing version, commit hash, and enabled built-ins
    • Parser now emits warnings for unsupported constructs (traditional loops, default imports/exports, with statement) instead of failing silently
  • Improvements

    • Unsupported language features now parse as no-ops with helpful warning messages and suggestions
    • Enhanced NaN and Infinity support in arithmetic operations

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the keyword system into reserved and contextual modules, introduces parser warnings for unsupported language constructs (loops, imports/exports, with statement), adds engine-global globalThis and GocciaScript objects with metadata, implements a version system via Git integration, enhances arithmetic operations for infinity/NaN handling, and adds comprehensive test coverage for global properties.

Changes

Cohort / File(s) Summary
Keyword System Refactor
units/Goccia.Keywords.Reserved.pas, units/Goccia.Keywords.Contextual.pas
Split JavaScript keywords from single module into reserved (break, case, class, const, for, if, let, var, while, etc.) and contextual (as, from, get, set, type, interface, etc.) keyword constants; ~55 keyword constants across two units.
Parser Warning Mechanism
units/Goccia.Parser.pas
Added TGocciaParserWarning type, GetWarnings() method, WarningCount property, and AddWarning() procedure. Extended ForStatement, WhileStatement, DoWhileStatement, ImportDeclaration, ExportDeclaration to emit warnings for unsupported syntax (loops, default/namespace/side-effect imports) while creating no-op AST nodes. Added SkipBlock and SkipStatementOrBlock helpers.
Engine Global Objects & Initialization
units/Goccia.Engine.pas
Added RegisterGlobalThis and RegisterGocciaScriptGlobal procedures to create engine-wide globals: globalThis (object containing all current bindings) and GocciaScript (object with version, commit, builtIns). Integrated PrintParserWarnings to output parser warnings after execution. Updated RegisterBuiltinConstructors to invoke global registration.
Versioning System
units/Goccia.Version.pas, build.pas
New Goccia.Version module provides GetVersion() and GetCommit() functions via Git (git describe, rev-parse). Caches results and defaults to 0.0.0-dev if unavailable. build.pas adds PrintVersion procedure to emit version/commit during build.
Arithmetic Operations Enhancement
units/Goccia.Evaluator.Arithmetic.pas
Introduced InfinityWithSign helper and ToNumericPair conversion. Rewrote EvaluateAddition, EvaluateSubtraction, EvaluateMultiplication, EvaluateDivision, EvaluateModulo, EvaluateExponentiation to centralize NaN/Infinity/zero handling with ECMAScript-like semantics including sign propagation and edge-case resolution.
Primitive Value Enhancements
units/Goccia.Values.Primitives.pas
Added GetIsInfinite helper and IsInfinite property to TGocciaNumberLiteralValue. Updated ToBooleanLiteral to treat NaN as false, infinities/non-zero numbers as true, and zero as false.
Scope and Type Operation Updates
units/Goccia.Scope.pas, units/Goccia.Evaluator.TypeOperations.pas
Introduced TLexicalBindingDictionary type alias and GetOwnBindingNames method returning dictionary keys. Updated keyword imports from Goccia.Keywords to Goccia.Keywords.Reserved.
Parser & Evaluator Keyword Reference Updates
units/Goccia.Lexer.pas, units/Goccia.Evaluator.pas, units/Goccia.Token.pas
Updated imports and InitKeywords in Lexer to use split keyword modules. Removed loop statement handling in Evaluator. Updated Token enum: removed gttUndefined, added gttVar and gttWith.
Global Property Test Suites
tests/language/global-properties/global-this.js, tests/language/global-properties/gocciascript.js, tests/language/global-properties/undefined.js, tests/language/global-properties/nan.js, tests/language/global-properties/infinity.js
Comprehensive test coverage for globalThis (self-reference, built-ins, constructors, immutability, shadowing), GocciaScript (version format, commit hash, builtIns array), and global primitives (undefined, NaN, Infinity) with immutability and scope shadowing scenarios.
Reserved Words Property Test
tests/language/identifiers/reserved-words-as-properties.js
Added test case verifying reserved word with can be used as an object property name.
Documentation Updates
docs/language-restrictions.md, docs/architecture.md, docs/built-ins.md, docs/design-decisions.md, docs/code-style.md, AGENTS.md
Updated documentation to reflect keyword split, globalThis and GocciaScript globals, unsupported feature warnings, version system, collection return type guidance, and expanded built-ins documentation.
Example & Benchmark Updates
examples/unsupported-features.js, benchmarks/promises.js
Added unsupported-features.js demonstrating var, loops, with, import/export as no-ops with warnings. Replaced loop-based promise chain construction with explicit 10-step chain in promises.js benchmark.
CI/CD Workflow Updates
.github/workflows/ci.yml, .github/workflows/pr.yml
Added fetch-depth: 0 to checkout steps across all jobs (build, test, benchmark, examples, artifacts, release) to enable full git history access for version/commit resolution.

Sequence Diagram(s)

sequenceDiagram
    participant Parser as GocciaParser
    participant Engine as TGocciaEngine
    participant Scope as GlobalScope
    participant Output as Stdout

    Parser->>Parser: ParseScript (e.g., for loop)
    alt Unsupported Construct
        Parser->>Parser: AddWarning(message, suggestion, line, col)
        Parser->>Parser: Create no-op statement
    end
    
    Engine->>Parser: Execute code
    Parser->>Engine: Return AST + Warnings
    
    Engine->>Engine: RegisterBuiltinConstructors()
    Engine->>Engine: RegisterGlobalThis()
    Note over Engine: Collect all global bindings<br/>into globalThis object
    Scope->>Scope: Bind globalThis as const
    
    Engine->>Engine: RegisterGocciaScriptGlobal()
    Note over Engine: Create GocciaScript with<br/>version, commit, builtIns
    Scope->>Scope: Bind GocciaScript as const
    
    Engine->>Engine: PrintParserWarnings(Parser)
    loop For each warning
        Engine->>Output: Print warning message, suggestion, location
    end
    
    Engine->>Output: Execution complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Named exports modules #13: Modifies import/export declaration parsing; this PR adds warning/no-op handling for unsupported import/export forms.
  • Type annotations as comments #14: Modifies parser to skip non-runtime constructs with AST metadata; shares similar empty-statement handling for type-only imports/exports.
  • Add Temporal API #3: Extends engine-global built-ins registration; both PRs enhance engine startup and global object initialization.

Poem

🐰 Keywords split like morning light,
Reserved and contextual, organized right,
Globals now whisper globalThis and GocciaScript's name,
Parser warns softly of unsupported flame,
Version tags dance through infinite arithmetic! 🎪

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Warning when using unsupported features' directly reflects the main objective of this changeset: implementing a warning system for unsupported JavaScript features.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-warning-when-using-unused-features

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@units/Goccia.Parser.pas`:
- Around line 161-166: TGocciaParser.GetWarnings uses Move to copy FWarnings
into Result which is unsafe because TGocciaParserWarning contains managed string
fields; replace the raw memory copy with an element-by-element assignment loop
so reference counts are updated correctly (keep SetLength(Result, FWarningCount)
and then for each index assign Result[i] := FWarnings[i]), referring to
TGocciaParser.GetWarnings, FWarnings and FWarningCount when updating the code.

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

github-actions Bot commented Feb 19, 2026

Benchmark Results

113 benchmarks · 🔴 1 regressed · 112 unchanged · avg -1.5%

arrays.js — 11 unchanged · avg -0.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 144,697 146,482 +1.2%
Array.of 10 elements 159,329 160,376 +0.7%
spread into new array 168,702 168,427 -0.2%
map over 50 elements 91,596 91,480 -0.1%
filter over 50 elements 91,196 90,964 -0.3%
reduce sum 50 elements 96,779 95,855 -1.0%
forEach over 50 elements 96,303 96,069 -0.2%
find in 50 elements 102,118 100,231 -1.8%
sort 20 elements 4,179 4,089 -2.2%
flat nested array 80,388 81,957 +2.0%
flatMap 39,850 40,282 +1.1%
classes.js — 10 unchanged · avg +0.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 74,534 74,975 +0.6%
class with defaults 57,163 57,047 -0.2%
50 instances via Array.from 79,770 81,612 +2.3%
instance method call 34,918 35,004 +0.2%
static method call 60,333 61,119 +1.3%
single-level inheritance 29,478 29,673 +0.7%
two-level inheritance 27,565 27,520 -0.2%
private field access 36,976 35,930 -2.8%
private methods 43,029 43,057 +0.1%
getter/setter access 38,752 38,612 -0.4%
closures.js — 11 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 57,880 57,561 -0.6%
closure over multiple variables 57,058 56,461 -1.0%
nested closures 61,735 61,299 -0.7%
function as argument 43,479 43,059 -1.0%
function returning function 57,569 57,137 -0.7%
compose two functions 35,143 34,679 -1.3%
fn.call 85,207 84,290 -1.1%
fn.apply 62,434 61,283 -1.8%
fn.bind 72,695 72,201 -0.7%
recursive sum to 50 5,028 5,030 +0.0%
recursive tree traversal 8,899 8,776 -1.4%
collections.js — 12 unchanged · avg -1.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 86,560 85,738 -0.9%
has lookup (50 elements) 67,170 66,300 -1.3%
delete elements 66,474 67,064 +0.9%
forEach iteration 83,219 82,112 -1.3%
spread to array 89,291 88,689 -0.7%
deduplicate array 56,755 55,475 -2.3%
set 50 entries 85,866 84,820 -1.2%
get lookup (50 entries) 65,072 63,139 -3.0%
has check 71,055 69,857 -1.7%
delete entries 65,433 64,490 -1.4%
forEach iteration 78,324 76,541 -2.3%
keys/values/entries 51,619 51,023 -1.2%
destructuring.js — 14 unchanged · avg -1.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 186,950 181,981 -2.7%
with rest element 136,917 133,491 -2.5%
with defaults 188,923 184,029 -2.6%
skip elements 203,365 197,579 -2.8%
nested array destructuring 112,181 112,457 +0.2%
swap variables 222,567 216,780 -2.6%
simple object destructuring 138,359 135,972 -1.7%
with defaults 165,450 163,243 -1.3%
with renaming 162,746 159,264 -2.1%
nested object destructuring 84,480 83,380 -1.3%
rest properties 86,220 85,196 -1.2%
object parameter 51,844 51,603 -0.5%
array parameter 66,958 66,451 -0.8%
mixed destructuring in map 89,888 88,989 -1.0%
fibonacci.js — 3 unchanged · avg -0.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 139 137 -1.1%
recursive fib(20) 12 12 -0.1%
iterative fib(20) via reduce 66,780 67,042 +0.4%
json.js — 11 unchanged · avg -1.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 113,792 111,586 -1.9%
parse nested object 72,952 72,411 -0.7%
parse array of objects 41,039 40,654 -0.9%
parse large flat object 35,261 34,398 -2.4%
parse mixed types 52,432 51,791 -1.2%
stringify simple object 100,082 99,713 -0.4%
stringify nested object 58,209 57,695 -0.9%
stringify array of objects 110,387 109,508 -0.8%
stringify mixed types 48,793 48,407 -0.8%
parse then stringify 35,201 34,960 -0.7%
stringify then parse 45,993 45,748 -0.5%
numbers.js — 11 unchanged · avg -1.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 187,313 178,124 -4.9%
floating point arithmetic 206,247 198,034 -4.0%
number coercion 105,032 104,348 -0.7%
toFixed 72,308 72,831 +0.7%
toString 100,284 98,601 -1.7%
valueOf 132,062 134,014 +1.5%
toPrecision 93,226 93,532 +0.3%
Number.isNaN 156,863 155,034 -1.2%
Number.isFinite 150,278 149,134 -0.8%
Number.isInteger 153,771 154,575 +0.5%
Number.parseInt and parseFloat 140,852 139,725 -0.8%
objects.js — 7 unchanged · avg -0.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 227,176 226,135 -0.5%
create nested object 119,939 121,089 +1.0%
create 50 objects via Array.from 142,700 141,914 -0.6%
property read 104,600 101,746 -2.7%
Object.keys 72,581 72,291 -0.4%
Object.entries 47,847 48,054 +0.4%
spread operator 91,731 92,043 +0.3%
promises.js — 🔴 1 regressed, 11 unchanged · avg -7.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 311,784 305,305 -2.1%
new Promise(resolve => resolve(value)) 121,524 120,976 -0.5%
Promise.reject(reason) 323,972 316,646 -2.3%
resolve + then (1 handler) 104,642 103,832 -0.8%
resolve + then chain (3 deep) 43,541 43,120 -1.0%
resolve + then chain (10 deep) 88,873 14,272 🔴 -83.9%
reject + catch + then 63,851 63,871 +0.0%
resolve + finally + then 56,521 56,866 +0.6%
Promise.all (5 resolved) 22,681 22,905 +1.0%
Promise.race (5 resolved) 23,735 24,203 +2.0%
Promise.allSettled (5 mixed) 18,372 18,613 +1.3%
Promise.any (5 mixed) 22,704 22,825 +0.5%
strings.js — 11 unchanged · avg -0.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 254,832 255,806 +0.4%
template literal 251,417 248,920 -1.0%
string repeat 242,979 236,472 -2.7%
split and join 103,211 102,916 -0.3%
indexOf and includes 106,930 106,795 -0.1%
toUpperCase and toLowerCase 157,094 157,156 +0.0%
slice and substring 95,473 95,604 +0.1%
trim operations 116,688 115,254 -1.2%
replace and replaceAll 135,668 134,570 -0.8%
startsWith and endsWith 85,430 85,716 +0.3%
padStart and padEnd 124,602 123,297 -1.0%

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (2)
units/Goccia.Values.Primitives.pas (2)

519-531: ⚠️ Potential issue | 🟡 Minor

Add test procedures for NaN and Infinity boolean coercion.

The test file units/Goccia.Values.Primitives.Test.pas lacks coverage for the new NaN/Infinity semantics in ToBooleanLiteral. Add test procedures that verify:

  • TGocciaNumberLiteralValue.NaNValue.ToBooleanLiteral returns FalseValue
  • TGocciaNumberLiteralValue.InfinityValue.ToBooleanLiteral returns TrueValue
  • TGocciaNumberLiteralValue.NegativeInfinityValue.ToBooleanLiteral returns TrueValue

Follow the existing pattern in TestNumberValue (around line 84) for structure and assertions.

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

In `@units/Goccia.Values.Primitives.pas` around lines 519 - 531, Add three unit
tests (matching the TestNumberValue pattern) that call
TGocciaNumberLiteralValue.NaNValue.ToBooleanLiteral,
.InfinityValue.ToBooleanLiteral and .NegativeInfinityValue.ToBooleanLiteral and
assert they return TGocciaBooleanLiteralValue.FalseValue, TrueValue and
TrueValue respectively; follow the existing TestNumberValue structure and
naming, create procedures in units/Goccia.Values.Primitives.Test.pas, invoke the
same assertion helpers used elsewhere in that test unit to compare the returned
TGocciaBooleanLiteralValue against the expected FalseValue/TrueValue results.

372-397: ⚠️ Potential issue | 🟠 Major

Add test cases for NaN/Infinity/negative zero construction; audit Array.sort for special value handling.

The constructor design is sound—normalizing special values to ZERO_VALUE while storing state in FSpecialValue is safe only if callers check IsNaN/IsInfinite properties first. However, the test suite has no coverage for direct special value construction, and ArrayValue.pas sorting compares .Value directly without checking for NaN, which will produce incorrect sort orders (NaN comparisons always yield false).

Add unit tests for:

  • TGocciaNumberLiteralValue.Create(NaN) → verify IsNaN and Value = 0
  • TGocciaNumberLiteralValue.Create(Infinity) → verify IsInfinite and Value = 0
  • TGocciaNumberLiteralValue.NegativeZeroValue → verify IsNegativeZero

For the sort comparator, follow the pattern in StringObjectValue.pas: check IsNaN/IsInfinite flags before accessing .Value, or use a comparison method that handles NaN semantically (e.g., NaN comparisons should sort to a fixed position, not treat 0 = 0 as equal).

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

In `@units/Goccia.Values.Primitives.pas` around lines 372 - 397, Tests are missing
for special-number construction and the array sort comparator in ArrayValue.pas
compares .Value directly (breaking on NaN/infinity/negative zero); add unit
tests asserting TGocciaNumberLiteralValue.Create(Math.NaN) yields IsNaN true and
Value = ZERO_VALUE, TGocciaNumberLiteralValue.Create(Math.Infinity) yields
IsInfinite true and Value = ZERO_VALUE, and the NegativeZero factory produces
IsNegativeZero true. Update the ArrayValue sort comparator to follow the pattern
in StringObjectValue.pas: consult
TGocciaNumberLiteralValue.IsNaN/IsInfinite/IsNegativeZero before using .Value,
implement a deterministic ordering for NaN/Infinity/−0 (e.g., NaN last,
-Infinity < normal < Infinity, treat negative zero distinct from positive zero)
and use that comparison routine for sorting instead of direct .Value
comparisons.
🧹 Nitpick comments (1)
.github/workflows/pr.yml (1)

49-51: Consider whether full history is needed for test and benchmark jobs.

Both jobs download pre-built artifacts and don't appear to require git history. If versioning is embedded during the build step, these jobs could use the default shallow clone (fetch-depth: 1) for slightly faster checkout times.

However, maintaining consistency across all jobs is also a valid approach, especially for a small repository where the performance difference is negligible.

Also applies to: 72-74

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

In @.github/workflows/pr.yml around lines 49 - 51, The checkout steps currently
use actions/checkout@v4 with fetch-depth: 0 (full history); for the test and
benchmark jobs referenced in the comment, change fetch-depth: 0 to fetch-depth:
1 to use a shallow clone (improves checkout speed) unless those jobs actually
require full git history for versioning or artifact operations—update the two
occurrences of actions/checkout@v4 (the checkout steps at the lines noted) to
set fetch-depth: 1, or explicitly document/centralize why fetch-depth: 0 must be
kept if consistency or build-embedded versioning requires full history.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@build.pas`:
- Around line 21-44: PrintVersion currently calls RunCommand for git describe
and rev-parse without checking for failure, causing empty Describe/Commit and
misleading output; update PrintVersion to check the result of each RunCommand
call (for the Describe and Commit invocations) and use safe fallbacks if the
command fails or returns empty (e.g., set Version to '0.0.0-dev' when Describe
is missing and Commit to 'unknown' when rev-parse fails), preserving the
existing trimming/substring logic for Describe and ensuring the final WriteLn
prints the fallback values; reference PrintVersion, RunCommand, Describe,
Commit, Version and adjust the control flow to handle command failure paths
consistently with other RunCommand usages in the file.

In `@tests/language/global-properties/nan.js`:
- Around line 1-121: Split this combined NaN test into three files: nan.happy.js
should contain the normal behavior tests (the describe "NaN as a global
property" block and the "NaN shadowing" tests that assert normal results such as
"global NaN has the correct value", "typeof NaN returns 'number'", propagation,
string conversion, shadowing cases including tests that use let/const/arrow
param/property/method names), nan.edge.js should contain non-fatal edge-case
assertions such as the falsy check ("NaN is falsy") and propagation through
arithmetic if you consider them edge, and nan.error.js should contain the
immutability/error cases from the "NaN immutability" describe block (tests
"global NaN cannot be reassigned" and "global NaN remains unchanged after failed
reassignment"). Preserve each test's content and names (e.g., the describe
blocks "NaN as a global property", "NaN immutability", "NaN shadowing" and test
names) so they remain identifiable, and update test runner imports if needed to
include the three new files.

In `@units/Goccia.Engine.pas`:
- Around line 311-349: Constructed TGocciaObjectValue/TGocciaArrayValue
instances in RegisterGlobalThis and RegisterGocciaScriptGlobal (GlobalThisObj,
BuiltInsArray, GocciaObj) are only referenced by local Pascal variables during
property assignment and may be collected by the GC; wrap their lifetime with
AddTempRoot immediately after creation and call RemoveTempRoot right after you
finish AssignProperty/DefineLexicalBinding (i.e., protect GlobalThisObj while
you copy Scope bindings and set 'globalThis' then remove the temp root after
Scope.DefineLexicalBinding; similarly protect BuiltInsArray and GocciaObj while
building and assigning properties and remove their temp roots after
FInterpreter.GlobalScope.DefineLexicalBinding). Ensure you call
AddTempRoot/RemoveTempRoot in matching pairs around the construction-to-binding
window to prevent premature collection.
- Around line 303-309: The globalThis snapshot is created in RegisterGlobalThis
before RegisterGocciaScriptGlobal runs, so the GocciaScript binding isn't
included on globalThis; move the RegisterGocciaScriptGlobal call to before
RegisterGlobalThis (i.e., call RegisterGocciaScriptGlobal, then
RegisterGlobalThis) so the GocciaScript binding is present on
globalThis.GocciaScript after the snapshot.

In `@units/Goccia.Evaluator.Arithmetic.pas`:
- Around line 174-183: The division logic mishandles IEEE-754 signed zeros: when
computing SameSign you use RightNum.Value >= 0 which treats +0 and -0 the same,
and when RightNum.IsInfinite you return +0 unconditionally; update checks to
detect negative zero (use an IsNegativeZero helper or evaluate 1/RightNum.Value
< 0) so InfinityWithSign(SameSign) uses the true sign of RightNum and
TGocciaNumberLiteralValue.ZeroValue is returned with the correct sign (negative
zero when operand signs dictate) in the RightNum.IsInfinite branch.
- Around line 249-256: The branch handling LeftNum.IsInfinite must first detect
non-integer exponents and return NaN for negative-infinity bases: if
LeftNum.IsInfinite and LeftNum.IsInfinity is true (negative infinity) and
RightNum.Value is non-integer (Frac(RightNum.Value) <> 0) then set Result to a
NaN numeric literal instead of applying the even/odd check; otherwise keep the
integer-exponent logic (use Frac(RightNum.Value / 2) = 0 only after confirming
the exponent is an integer) and preserve the existing InfinityWithSign and
TGocciaNumberLiteralValue.ZeroValue outcomes.
- Around line 258-267: The infinite-exponent branch conflates |base|=1 and sign
handling; update the RightNum.IsInfinite block to handle three cases: if
Abs(LeftNum.Value)=1 then return TGocciaNumberLiteralValue.OneValue when
LeftNum.Value = 1 and TGocciaNumberLiteralValue.NaNValue when LeftNum.Value =
-1; else if Abs(LeftNum.Value) > 1 then return
InfinityWithSign(RightNum.IsInfinity) for +Infinity and
TGocciaNumberLiteralValue.ZeroValue for RightNum.IsNegativeInfinity; else
(Abs(LeftNum.Value) < 1) return TGocciaNumberLiteralValue.ZeroValue for
RightNum.IsInfinity and InfinityWithSign(RightNum.IsNegativeInfinity) for
RightNum.IsNegativeInfinity; use LeftNum.Value, RightNum.IsInfinite,
RightNum.IsInfinity, RightNum.IsNegativeInfinity,
TGocciaNumberLiteralValue.ZeroValue/OneValue/NaNValue and InfinityWithSign to
implement this logic.

In `@units/Goccia.Parser.pas`:
- Around line 1357-1479: Skip logic in SkipBlock/SkipStatementOrBlock and in
VarStatement/ForStatement/WhileStatement/DoWhileStatement/WithStatement
currently scans to the first ')' or uses SkipUntilSemicolon which can desync on
nested parentheses and compound bodies; replace the ad-hoc scans by (1) adding a
balanced-paren helper (e.g. SkipBalancedParens) used by
ForStatement/WhileStatement/DoWhileStatement/WithStatement instead of looping
until the first gttRightParen, and (2) change SkipStatementOrBlock to actually
parse a single statement body (call the existing statement parsing routine to
build the statement and then free it) rather than calling SkipUntilSemicolon, so
recovery handles nested blocks, if/else/try/switch correctly. Ensure SkipBlock
remains balanced for braces and use these helpers from the referenced methods to
avoid over-skipping.

In `@units/Goccia.Version.pas`:
- Around line 38-57: In Initialize, after calling RunGit to set CachedCommit,
add a fallback when CachedCommit is empty (non-git or git unavailable) by
assigning a non-empty placeholder (e.g. 'unknown' or '0000000') so tests
expecting GocciaScript.commit.length > 0 pass; update the procedure that calls
RunGit(['rev-parse', '--short', 'HEAD']) and set CachedCommit to the placeholder
if it returns an empty string, leaving the existing CachedVersion logic
unchanged.

---

Outside diff comments:
In `@units/Goccia.Values.Primitives.pas`:
- Around line 519-531: Add three unit tests (matching the TestNumberValue
pattern) that call TGocciaNumberLiteralValue.NaNValue.ToBooleanLiteral,
.InfinityValue.ToBooleanLiteral and .NegativeInfinityValue.ToBooleanLiteral and
assert they return TGocciaBooleanLiteralValue.FalseValue, TrueValue and
TrueValue respectively; follow the existing TestNumberValue structure and
naming, create procedures in units/Goccia.Values.Primitives.Test.pas, invoke the
same assertion helpers used elsewhere in that test unit to compare the returned
TGocciaBooleanLiteralValue against the expected FalseValue/TrueValue results.
- Around line 372-397: Tests are missing for special-number construction and the
array sort comparator in ArrayValue.pas compares .Value directly (breaking on
NaN/infinity/negative zero); add unit tests asserting
TGocciaNumberLiteralValue.Create(Math.NaN) yields IsNaN true and Value =
ZERO_VALUE, TGocciaNumberLiteralValue.Create(Math.Infinity) yields IsInfinite
true and Value = ZERO_VALUE, and the NegativeZero factory produces
IsNegativeZero true. Update the ArrayValue sort comparator to follow the pattern
in StringObjectValue.pas: consult
TGocciaNumberLiteralValue.IsNaN/IsInfinite/IsNegativeZero before using .Value,
implement a deterministic ordering for NaN/Infinity/−0 (e.g., NaN last,
-Infinity < normal < Infinity, treat negative zero distinct from positive zero)
and use that comparison routine for sorting instead of direct .Value
comparisons.

---

Duplicate comments:
In `@tests/language/global-properties/infinity.js`:
- Around line 1-147: Split this single test file into three files following the
project test-structure: a happy-path file containing normal behavior checks
(tests titled "global Infinity has the correct value", "typeof Infinity returns
'number'", "Infinity is equal to itself", "negative Infinity", "Infinity is
truthy", "negative Infinity is truthy", "arithmetic with Infinity", "Infinity
converts to 'Infinity' string", "Number.isFinite rejects Infinity", and the
"Infinity as a property/method name works" and "arrow function parameter can
shadow Infinity" tests), an edge-cases file for shadowing and scoping behavior
(tests under "Infinity shadowing" such as "Infinity can be shadowed in a block
scope with let/const", "shadowed Infinity does not affect outer scope", "nested
shadowing restores correctly", and the "Infinity as a property name works" if
considered non-error edge), and an errors file for mutation/reassignment failure
checks (tests under "Infinity immutability": "global Infinity cannot be
reassigned" and "global Infinity remains unchanged after failed reassignment");
ensure each new file keeps the same test names and descriptions,
imports/describe blocks, and that tests referencing Number.isFinite or using
try/catch remain unchanged so assertions still run as before.

In `@tests/language/global-properties/undefined.js`:
- Around line 1-150: Split this combined test into three files: a happy-path
file containing stable non-error behaviors (move tests from describe("undefined
as a global property") and non-failing shadowing tests such as "undefined can be
shadowed in a block scope with let/const", "arrow function parameter can shadow
undefined", "undefined as a property name works", "undefined as a method name
works", and "undefined shadowing" cases that assert values/types), an edge-cases
file for borderline behaviors (move nested/shadowing variations like "nested
shadowing restores correctly" and the "shadowed undefined can be any type" group
and arithmetic/string coercion tests like Number.isNaN and `${undefined}`), and
an error-cases file for tests that assert thrown behavior (move
describe("undefined immutability") tests such as "global undefined cannot be
reassigned" and related reassignment-try/catch checks). Keep the original
describe blocks and test names (e.g., describe("undefined as a global
property"), describe("undefined immutability"), describe("undefined shadowing"))
to locate and extract tests, and ensure each new file follows the existing test
header/comments and naming convention (e.g., undefined/happy.js,
undefined/edge.js, undefined/error.js).

---

Nitpick comments:
In @.github/workflows/pr.yml:
- Around line 49-51: The checkout steps currently use actions/checkout@v4 with
fetch-depth: 0 (full history); for the test and benchmark jobs referenced in the
comment, change fetch-depth: 0 to fetch-depth: 1 to use a shallow clone
(improves checkout speed) unless those jobs actually require full git history
for versioning or artifact operations—update the two occurrences of
actions/checkout@v4 (the checkout steps at the lines noted) to set fetch-depth:
1, or explicitly document/centralize why fetch-depth: 0 must be kept if
consistency or build-embedded versioning requires full history.

Comment thread build.pas
Comment on lines +21 to +44
procedure PrintVersion;
var
Describe, Commit, Version: string;
DashPos: SizeInt;
begin
RunCommand('git', ['describe', '--tags', '--always'], Describe);
Describe := Trim(Describe);
RunCommand('git', ['rev-parse', '--short', 'HEAD'], Commit);
Commit := Trim(Commit);

if Describe = '' then
Version := '0.0.0-dev'
else
begin
DashPos := Pos('-', Describe);
if DashPos > 0 then
Version := Copy(Describe, 1, DashPos - 1) + '-dev'
else
Version := Describe;
end;

WriteLn('Version: ', Version, ' (', Commit, ')');
WriteLn('');
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n --type=pas -C2 "RunCommand\("

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

# Find the RunCommand procedure/function definition
rg -n --type=pas "^(function|procedure)\s+RunCommand\s*\("

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

# Check build.pas structure and context around line 249
cat -n build.pas | head -260 | tail -40

Repository: frostney/GocciaScript

Length of output: 1222


🏁 Script executed:

# Search for RunCommand in Pascal files
rg -n "RunCommand\(" --glob="*.pas" -C2

Repository: frostney/GocciaScript

Length of output: 1684


🏁 Script executed:

# Find RunCommand definition
rg -n "procedure RunCommand|function RunCommand" --glob="*.pas"

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Check complete build.pas for error handling context
fd "build.pas" -x wc -l {}

Repository: frostney/GocciaScript

Length of output: 80


🏁 Script executed:

# Show exact lines 21-44 from build.pas with line numbers
sed -n '21,44p' build.pas | cat -n --vt100

Repository: frostney/GocciaScript

Length of output: 138


🏁 Script executed:

# Show the full context of how Describe is used after assignment
sed -n '21,45p' build.pas | cat -n

Repository: frostney/GocciaScript

Length of output: 816


Add error handling to git commands in PrintVersion to avoid misleading version output.

The RunCommand calls at lines 26 and 28 lack error checks, inconsistent with every other RunCommand usage in this file (lines 94, 110, 142, 159, 174). If git commands fail (missing git, shallow clone, etc.), the output will display an empty Commit string instead of a fallback value.

Suggested fix
-  RunCommand('git', ['describe', '--tags', '--always'], Describe);
-  Describe := Trim(Describe);
-  RunCommand('git', ['rev-parse', '--short', 'HEAD'], Commit);
-  Commit := Trim(Commit);
+  if not RunCommand('git', ['describe', '--tags', '--always'], Describe) then
+    Describe := '';
+  Describe := Trim(Describe);
+
+  if not RunCommand('git', ['rev-parse', '--short', 'HEAD'], Commit) then
+    Commit := 'unknown';
+  Commit := Trim(Commit);
+  if Commit = '' then
+    Commit := 'unknown';
📝 Committable suggestion

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

Suggested change
procedure PrintVersion;
var
Describe, Commit, Version: string;
DashPos: SizeInt;
begin
RunCommand('git', ['describe', '--tags', '--always'], Describe);
Describe := Trim(Describe);
RunCommand('git', ['rev-parse', '--short', 'HEAD'], Commit);
Commit := Trim(Commit);
if Describe = '' then
Version := '0.0.0-dev'
else
begin
DashPos := Pos('-', Describe);
if DashPos > 0 then
Version := Copy(Describe, 1, DashPos - 1) + '-dev'
else
Version := Describe;
end;
WriteLn('Version: ', Version, ' (', Commit, ')');
WriteLn('');
end;
procedure PrintVersion;
var
Describe, Commit, Version: string;
DashPos: SizeInt;
begin
if not RunCommand('git', ['describe', '--tags', '--always'], Describe) then
Describe := '';
Describe := Trim(Describe);
if not RunCommand('git', ['rev-parse', '--short', 'HEAD'], Commit) then
Commit := 'unknown';
Commit := Trim(Commit);
if Commit = '' then
Commit := 'unknown';
if Describe = '' then
Version := '0.0.0-dev'
else
begin
DashPos := Pos('-', Describe);
if DashPos > 0 then
Version := Copy(Describe, 1, DashPos - 1) + '-dev'
else
Version := Describe;
end;
WriteLn('Version: ', Version, ' (', Commit, ')');
WriteLn('');
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.pas` around lines 21 - 44, PrintVersion currently calls RunCommand for
git describe and rev-parse without checking for failure, causing empty
Describe/Commit and misleading output; update PrintVersion to check the result
of each RunCommand call (for the Describe and Commit invocations) and use safe
fallbacks if the command fails or returns empty (e.g., set Version to
'0.0.0-dev' when Describe is missing and Commit to 'unknown' when rev-parse
fails), preserving the existing trimming/substring logic for Describe and
ensuring the final WriteLn prints the fallback values; reference PrintVersion,
RunCommand, Describe, Commit, Version and adjust the control flow to handle
command failure paths consistently with other RunCommand usages in the file.

Comment on lines +1 to +121
/*---
description: >
NaN is a global property (not a keyword) per ECMAScript spec.
It can be shadowed by local variable declarations.
features: [global-properties, NaN, shadowing]
---*/

describe("NaN as a global property", () => {
test("global NaN has the correct value", () => {
expect(Number.isNaN(NaN)).toBe(true);
});

test("typeof NaN returns 'number'", () => {
expect(typeof NaN).toBe("number");
});

test("NaN is not equal to itself", () => {
expect(NaN === NaN).toBe(false);
expect(NaN !== NaN).toBe(true);
});

test("NaN is falsy", () => {
let result = "truthy";
if (!NaN) {
result = "falsy";
}
expect(result).toBe("falsy");
});

test("NaN propagates through arithmetic", () => {
expect(Number.isNaN(NaN + 1)).toBe(true);
expect(Number.isNaN(NaN * 2)).toBe(true);
expect(Number.isNaN(NaN - 0)).toBe(true);
expect(Number.isNaN(NaN / 10)).toBe(true);
});

test("NaN converts to 'NaN' string", () => {
expect(`${NaN}`).toBe("NaN");
expect("" + NaN).toBe("NaN");
});
});

describe("NaN immutability", () => {
test("global NaN cannot be reassigned", () => {
expect(() => {
NaN = 42;
}).toThrow();
});

test("global NaN remains unchanged after failed reassignment", () => {
try {
NaN = 0;
} catch (e) {
// expected
}
expect(Number.isNaN(NaN)).toBe(true);
});
});

describe("NaN shadowing", () => {
test("NaN can be shadowed in a block scope with let", () => {
expect(Number.isNaN(NaN)).toBe(true);
{
let NaN = 42;
expect(NaN).toBe(42);
}
expect(Number.isNaN(NaN)).toBe(true);
});

test("NaN can be shadowed in a block scope with const", () => {
{
const NaN = "not a number";
expect(NaN).toBe("not a number");
}
expect(Number.isNaN(NaN)).toBe(true);
});

test("shadowed NaN does not affect outer scope", () => {
const outer = NaN;
{
let NaN = 100;
expect(NaN).toBe(100);
}
expect(Number.isNaN(outer)).toBe(true);
});

test("nested shadowing restores correctly", () => {
expect(Number.isNaN(NaN)).toBe(true);
{
let NaN = 1;
expect(NaN).toBe(1);
{
let NaN = 2;
expect(NaN).toBe(2);
}
expect(NaN).toBe(1);
}
expect(Number.isNaN(NaN)).toBe(true);
});

test("arrow function parameter can shadow NaN", () => {
const fn = (NaN) => NaN;
expect(fn(42)).toBe(42);
expect(fn("test")).toBe("test");
});

test("NaN as a property name works", () => {
const obj = { NaN: "prop-value" };
expect(obj.NaN).toBe("prop-value");
expect(obj["NaN"]).toBe("prop-value");
});

test("NaN as a method name works", () => {
const obj = {
NaN() {
return "method-value";
},
};
expect(obj.NaN()).toBe("method-value");
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Split NaN tests into happy/edge/error files.

This file mixes happy paths, edge cases, and error cases. Please split into separate files (e.g., nan.happy.js, nan.edge.js, nan.error.js) following the existing test structure.
As per coding guidelines, "When implementing a new language feature, create JavaScript test files under the tests/ directory following the existing directory structure, with separate files for happy paths, edge cases, and error cases."

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

In `@tests/language/global-properties/nan.js` around lines 1 - 121, Split this
combined NaN test into three files: nan.happy.js should contain the normal
behavior tests (the describe "NaN as a global property" block and the "NaN
shadowing" tests that assert normal results such as "global NaN has the correct
value", "typeof NaN returns 'number'", propagation, string conversion, shadowing
cases including tests that use let/const/arrow param/property/method names),
nan.edge.js should contain non-fatal edge-case assertions such as the falsy
check ("NaN is falsy") and propagation through arithmetic if you consider them
edge, and nan.error.js should contain the immutability/error cases from the "NaN
immutability" describe block (tests "global NaN cannot be reassigned" and
"global NaN remains unchanged after failed reassignment"). Preserve each test's
content and names (e.g., the describe blocks "NaN as a global property", "NaN
immutability", "NaN shadowing" and test names) so they remain identifiable, and
update test runner imports if needed to include the three new files.

Comment thread units/Goccia.Engine.pas
Comment on lines 303 to +309
// Create Function constructor
FunctionConstructor := TGocciaClassValue.Create('Function', nil);
FInterpreter.GlobalScope.DefineLexicalBinding('Function', FunctionConstructor, dtConst);

RegisterGlobalThis;
RegisterGocciaScriptGlobal;
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the Goccia.Engine.pas file around the mentioned lines
cd /tmp && find . -name "Goccia.Engine.pas" 2>/dev/null | head -5

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Search for the file location
find . -type f -name "*.pas" | grep -i engine | head -10

Repository: frostney/GocciaScript

Length of output: 90


🏁 Script executed:

# Let's also check the repository structure
git ls-files | grep -E "(Engine|\.pas$)" | head -20

Repository: frostney/GocciaScript

Length of output: 712


🏁 Script executed:

# Read the Goccia.Engine.pas file to examine the registration methods
wc -l ./units/Goccia.Engine.pas

Repository: frostney/GocciaScript

Length of output: 94


🏁 Script executed:

# Read the relevant section around lines 303-349
sed -n '290,360p' ./units/Goccia.Engine.pas

Repository: frostney/GocciaScript

Length of output: 2571


🏁 Script executed:

# Search for all globalThis modifications and usage patterns
rg -n "globalThis" --type pas --type js --type goccia

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

# Search for GocciaScript usage to understand if it should be in globalThis
rg -n "GocciaScript" --type pas --type js --type goccia

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

# Check if there's any code that patches globalThis after these registrations
sed -n '1,480p' ./units/Goccia.Engine.pas | grep -n "globalThis\|GocciaScript"

Repository: frostney/GocciaScript

Length of output: 2188


🏁 Script executed:

# Search for globalThis and GocciaScript without file type restrictions
rg -n "globalThis" -g '*.pas' -g '*.js' -g '*.goccia'

Repository: frostney/GocciaScript

Length of output: 7356


🏁 Script executed:

# Search for GocciaScript usage
rg -n "GocciaScript" -g '*.pas' -g '*.js' -g '*.goccia'

Repository: frostney/GocciaScript

Length of output: 6233


🏁 Script executed:

# Look for any test files that might reveal expected behavior
find . -type f \( -name "*test*" -o -name "*Test*" \) | head -20

Repository: frostney/GocciaScript

Length of output: 362


🏁 Script executed:

# Check if any tests or code explicitly verify globalThis.GocciaScript
rg -n "globalThis\.GocciaScript" -g '*.js' -g '*.pas' -g '*.goccia'

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Check if there's any code that adds GocciaScript to globalThis after registration
sed -n '300,360p' ./units/Goccia.Engine.pas | grep -A 50 "RegisterGocciaScriptGlobal"

Repository: frostney/GocciaScript

Length of output: 1723


🏁 Script executed:

# Look at the full initialization sequence to confirm no patching occurs elsewhere
sed -n '260,360p' ./units/Goccia.Engine.pas

Repository: frostney/GocciaScript

Length of output: 3954


globalThis snapshot taken before GocciaScript registration.

RegisterGlobalThis creates a snapshot of the current scope's bindings (line 319) and assigns them to the globalThis object. At that point, GocciaScript has not yet been registered. Swapping the call order on lines 307–308 would make GocciaScript accessible via globalThis.GocciaScript; otherwise, it remains accessible only as a top-level binding.

Suggested fix
-  RegisterGlobalThis;
-  RegisterGocciaScriptGlobal;
+  RegisterGocciaScriptGlobal;
+  RegisterGlobalThis;
📝 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
// Create Function constructor
FunctionConstructor := TGocciaClassValue.Create('Function', nil);
FInterpreter.GlobalScope.DefineLexicalBinding('Function', FunctionConstructor, dtConst);
RegisterGlobalThis;
RegisterGocciaScriptGlobal;
end;
// Create Function constructor
FunctionConstructor := TGocciaClassValue.Create('Function', nil);
FInterpreter.GlobalScope.DefineLexicalBinding('Function', FunctionConstructor, dtConst);
RegisterGocciaScriptGlobal;
RegisterGlobalThis;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Engine.pas` around lines 303 - 309, The globalThis snapshot is
created in RegisterGlobalThis before RegisterGocciaScriptGlobal runs, so the
GocciaScript binding isn't included on globalThis; move the
RegisterGocciaScriptGlobal call to before RegisterGlobalThis (i.e., call
RegisterGocciaScriptGlobal, then RegisterGlobalThis) so the GocciaScript binding
is present on globalThis.GocciaScript after the snapshot.

Comment thread units/Goccia.Engine.pas
Comment on lines +311 to 349
procedure TGocciaEngine.RegisterGlobalThis;
var
GlobalThisObj: TGocciaObjectValue;
Scope: TGocciaScope;
Name: string;
begin
Scope := FInterpreter.GlobalScope;
GlobalThisObj := TGocciaObjectValue.Create;

for Name in Scope.GetOwnBindingNames do
GlobalThisObj.AssignProperty(Name, Scope.GetValue(Name));

GlobalThisObj.AssignProperty('globalThis', GlobalThisObj);
Scope.DefineLexicalBinding('globalThis', GlobalThisObj, dtConst);
end;

procedure TGocciaEngine.RegisterGocciaScriptGlobal;
const
PREFIX_LENGTH = 2; // Strip 'gg' prefix from enum names
var
GocciaObj: TGocciaObjectValue;
BuiltInsArray: TGocciaArrayValue;
Flag: TGocciaGlobalBuiltin;
Name: string;
begin
BuiltInsArray := TGocciaArrayValue.Create;
for Flag in FGlobals do
begin
Name := GetEnumName(TypeInfo(TGocciaGlobalBuiltin), Ord(Flag));
BuiltInsArray.Elements.Add(TGocciaStringLiteralValue.Create(Copy(Name, PREFIX_LENGTH + 1, Length(Name) - PREFIX_LENGTH)));
end;

GocciaObj := TGocciaObjectValue.Create;
GocciaObj.AssignProperty('version', TGocciaStringLiteralValue.Create(GetVersion));
GocciaObj.AssignProperty('commit', TGocciaStringLiteralValue.Create(GetCommit));
GocciaObj.AssignProperty('builtIns', BuiltInsArray);

FInterpreter.GlobalScope.DefineLexicalBinding('GocciaScript', GocciaObj, dtConst);
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

Temp-root new values during global registration.

Line 318-348 allocates TGocciaObjectValue/TGocciaArrayValue instances that are only held in local Pascal variables until the scope bindings are created. If GC runs during property assignment, these values can be collected. Please wrap the construction phase with AddTempRoot/RemoveTempRoot.

🛠️ Suggested fix
 procedure TGocciaEngine.RegisterGlobalThis;
 var
   GlobalThisObj: TGocciaObjectValue;
   Scope: TGocciaScope;
   Name: string;
 begin
   Scope := FInterpreter.GlobalScope;
   GlobalThisObj := TGocciaObjectValue.Create;
+  TGocciaGarbageCollector.Instance.AddTempRoot(GlobalThisObj);
+  try
-  for Name in Scope.GetOwnBindingNames do
-    GlobalThisObj.AssignProperty(Name, Scope.GetValue(Name));
-
-  GlobalThisObj.AssignProperty('globalThis', GlobalThisObj);
-  Scope.DefineLexicalBinding('globalThis', GlobalThisObj, dtConst);
+    for Name in Scope.GetOwnBindingNames do
+      GlobalThisObj.AssignProperty(Name, Scope.GetValue(Name));
+
+    GlobalThisObj.AssignProperty('globalThis', GlobalThisObj);
+    Scope.DefineLexicalBinding('globalThis', GlobalThisObj, dtConst);
+  finally
+    TGocciaGarbageCollector.Instance.RemoveTempRoot(GlobalThisObj);
+  end;
 end;
 procedure TGocciaEngine.RegisterGocciaScriptGlobal;
 const
   PREFIX_LENGTH = 2; // Strip 'gg' prefix from enum names
 var
   GocciaObj: TGocciaObjectValue;
   BuiltInsArray: TGocciaArrayValue;
   Flag: TGocciaGlobalBuiltin;
   Name: string;
 begin
   BuiltInsArray := TGocciaArrayValue.Create;
+  TGocciaGarbageCollector.Instance.AddTempRoot(BuiltInsArray);
+  try
   for Flag in FGlobals do
   begin
     Name := GetEnumName(TypeInfo(TGocciaGlobalBuiltin), Ord(Flag));
     BuiltInsArray.Elements.Add(TGocciaStringLiteralValue.Create(Copy(Name, PREFIX_LENGTH + 1, Length(Name) - PREFIX_LENGTH)));
   end;
 
   GocciaObj := TGocciaObjectValue.Create;
-  GocciaObj.AssignProperty('version', TGocciaStringLiteralValue.Create(GetVersion));
-  GocciaObj.AssignProperty('commit', TGocciaStringLiteralValue.Create(GetCommit));
-  GocciaObj.AssignProperty('builtIns', BuiltInsArray);
-
-  FInterpreter.GlobalScope.DefineLexicalBinding('GocciaScript', GocciaObj, dtConst);
+  TGocciaGarbageCollector.Instance.AddTempRoot(GocciaObj);
+  try
+    GocciaObj.AssignProperty('version', TGocciaStringLiteralValue.Create(GetVersion));
+    GocciaObj.AssignProperty('commit', TGocciaStringLiteralValue.Create(GetCommit));
+    GocciaObj.AssignProperty('builtIns', BuiltInsArray);
+
+    FInterpreter.GlobalScope.DefineLexicalBinding('GocciaScript', GocciaObj, dtConst);
+  finally
+    TGocciaGarbageCollector.Instance.RemoveTempRoot(GocciaObj);
+  end;
+  finally
+    TGocciaGarbageCollector.Instance.RemoveTempRoot(BuiltInsArray);
+  end;
 end;
As per coding guidelines: Values held only by Pascal code and not in any GocciaScript scope must be protected with `AddTempRoot`/`RemoveTempRoot` for the duration they are needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Engine.pas` around lines 311 - 349, Constructed
TGocciaObjectValue/TGocciaArrayValue instances in RegisterGlobalThis and
RegisterGocciaScriptGlobal (GlobalThisObj, BuiltInsArray, GocciaObj) are only
referenced by local Pascal variables during property assignment and may be
collected by the GC; wrap their lifetime with AddTempRoot immediately after
creation and call RemoveTempRoot right after you finish
AssignProperty/DefineLexicalBinding (i.e., protect GlobalThisObj while you copy
Scope bindings and set 'globalThis' then remove the temp root after
Scope.DefineLexicalBinding; similarly protect BuiltInsArray and GocciaObj while
building and assigning properties and remove their temp roots after
FInterpreter.GlobalScope.DefineLexicalBinding). Ensure you call
AddTempRoot/RemoveTempRoot in matching pairs around the construction-to-binding
window to prevent premature collection.

Comment thread units/Goccia.Evaluator.Arithmetic.pas Outdated
Comment on lines 174 to 183
SameSign := LeftNum.IsInfinity = (RightNum.Value >= 0);
Result := InfinityWithSign(SameSign);
Exit;
end;

if LeftNum.IsNaN or RightNum.IsNaN then
if RightNum.IsInfinite then
begin
Result := TGocciaNumberLiteralValue.NaNValue;
Result := TGocciaNumberLiteralValue.ZeroValue;
Exit;
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Negative zero handling may produce incorrect sign.

The sign checks don't account for IEEE 754 negative zero:

  1. Line 174: RightNum.Value >= 0 evaluates true for both +0 and -0, so Infinity / (-0) incorrectly yields +Infinity instead of -Infinity.

  2. Line 181: finite / ±Infinity always returns +0, but ECMAScript requires signed zero based on operand signs (e.g., 5 / (-Infinity) = -0).

If signed-zero correctness is a goal, consider using IsNegativeZero helper or 1/Value < 0 to detect -0.

💡 Sketch for handling signed zero in divisor
+function IsNegativeZero(const AValue: Double): Boolean; inline;
+begin
+  Result := (AValue = 0) and ((1.0 / AValue) < 0);
+end;
+
 // In EvaluateDivision, line 174:
-    SameSign := LeftNum.IsInfinity = (RightNum.Value >= 0);
+    SameSign := LeftNum.IsInfinity xor IsNegativeZero(RightNum.Value);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Evaluator.Arithmetic.pas` around lines 174 - 183, The division
logic mishandles IEEE-754 signed zeros: when computing SameSign you use
RightNum.Value >= 0 which treats +0 and -0 the same, and when
RightNum.IsInfinite you return +0 unconditionally; update checks to detect
negative zero (use an IsNegativeZero helper or evaluate 1/RightNum.Value < 0) so
InfinityWithSign(SameSign) uses the true sign of RightNum and
TGocciaNumberLiteralValue.ZeroValue is returned with the correct sign (negative
zero when operand signs dictate) in the RightNum.IsInfinite branch.

Comment thread units/Goccia.Evaluator.Arithmetic.pas Outdated
Comment on lines +249 to +256
if LeftNum.IsInfinite then
begin
if RightNum.Value > 0 then
Result := InfinityWithSign(LeftNum.IsInfinity or (Frac(RightNum.Value / 2) = 0))
else
Result := TGocciaNumberLiteralValue.ZeroValue;
Exit;
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

Fractional exponents with negative infinity base yield incorrect result.

When base is -Infinity and exponent is a non-integer (e.g., 0.5), the result should be NaN (complex number domain), but the current logic treats all exponents uniformly:

(-Infinity) ** 0.5  // NaN in ECMAScript
(-Infinity) ** 2    // Infinity
(-Infinity) ** 3    // -Infinity

The Frac(RightNum.Value / 2) = 0 check determines even vs. odd but doesn't distinguish integer from fractional exponents.

🐛 Proposed fix to handle fractional exponents
   if LeftNum.IsInfinite then
   begin
+    // Fractional exponent with negative base → NaN
+    if (not LeftNum.IsInfinity) and (Frac(RightNum.Value) <> 0) then
+    begin
+      Result := TGocciaNumberLiteralValue.NaNValue;
+      Exit;
+    end;
     if RightNum.Value > 0 then
       Result := InfinityWithSign(LeftNum.IsInfinity or (Frac(RightNum.Value / 2) = 0))
     else
       Result := TGocciaNumberLiteralValue.ZeroValue;
     Exit;
   end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Evaluator.Arithmetic.pas` around lines 249 - 256, The branch
handling LeftNum.IsInfinite must first detect non-integer exponents and return
NaN for negative-infinity bases: if LeftNum.IsInfinite and LeftNum.IsInfinity is
true (negative infinity) and RightNum.Value is non-integer (Frac(RightNum.Value)
<> 0) then set Result to a NaN numeric literal instead of applying the even/odd
check; otherwise keep the integer-exponent logic (use Frac(RightNum.Value / 2) =
0 only after confirming the exponent is an integer) and preserve the existing
InfinityWithSign and TGocciaNumberLiteralValue.ZeroValue outcomes.

Comment thread units/Goccia.Evaluator.Arithmetic.pas Outdated
Comment on lines +258 to +267
if RightNum.IsInfinite then
begin
if Abs(LeftNum.Value) = 1 then
Result := TGocciaNumberLiteralValue.NaNValue
else if Abs(LeftNum.Value) > 1 then
Result := InfinityWithSign(RightNum.IsInfinity)
else
Result := InfinityWithSign(RightNum.IsNegativeInfinity);
Exit;
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

Infinite exponent edge cases produce incorrect results.

Several ECMAScript edge cases are not handled correctly:

Expression Expected Current
1 ** Infinity 1 NaN
2 ** (-Infinity) 0 -Infinity
0.5 ** Infinity 0 -Infinity

The logic conflates |base| = 1 (should return 1 for base exactly 1, NaN for -1) and incorrectly returns infinity instead of zero for convergent cases.

🐛 Proposed fix for infinite exponent handling
   if RightNum.IsInfinite then
   begin
-    if Abs(LeftNum.Value) = 1 then
-      Result := TGocciaNumberLiteralValue.NaNValue
-    else if Abs(LeftNum.Value) > 1 then
-      Result := InfinityWithSign(RightNum.IsInfinity)
+    if LeftNum.Value = 1 then
+      Result := TGocciaNumberLiteralValue.OneValue
+    else if LeftNum.Value = -1 then
+      Result := TGocciaNumberLiteralValue.NaNValue
+    else if Abs(LeftNum.Value) > 1 then
+    begin
+      if RightNum.IsInfinity then
+        Result := TGocciaNumberLiteralValue.InfinityValue
+      else
+        Result := TGocciaNumberLiteralValue.ZeroValue;
+    end
     else
-      Result := InfinityWithSign(RightNum.IsNegativeInfinity);
+    begin
+      if RightNum.IsInfinity then
+        Result := TGocciaNumberLiteralValue.ZeroValue
+      else
+        Result := TGocciaNumberLiteralValue.InfinityValue;
+    end;
     Exit;
   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
if RightNum.IsInfinite then
begin
if Abs(LeftNum.Value) = 1 then
Result := TGocciaNumberLiteralValue.NaNValue
else if Abs(LeftNum.Value) > 1 then
Result := InfinityWithSign(RightNum.IsInfinity)
else
Result := InfinityWithSign(RightNum.IsNegativeInfinity);
Exit;
end;
if RightNum.IsInfinite then
begin
if LeftNum.Value = 1 then
Result := TGocciaNumberLiteralValue.OneValue
else if LeftNum.Value = -1 then
Result := TGocciaNumberLiteralValue.NaNValue
else if Abs(LeftNum.Value) > 1 then
begin
if RightNum.IsInfinity then
Result := TGocciaNumberLiteralValue.InfinityValue
else
Result := TGocciaNumberLiteralValue.ZeroValue;
end
else
begin
if RightNum.IsInfinity then
Result := TGocciaNumberLiteralValue.ZeroValue
else
Result := TGocciaNumberLiteralValue.InfinityValue;
end;
Exit;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Evaluator.Arithmetic.pas` around lines 258 - 267, The
infinite-exponent branch conflates |base|=1 and sign handling; update the
RightNum.IsInfinite block to handle three cases: if Abs(LeftNum.Value)=1 then
return TGocciaNumberLiteralValue.OneValue when LeftNum.Value = 1 and
TGocciaNumberLiteralValue.NaNValue when LeftNum.Value = -1; else if
Abs(LeftNum.Value) > 1 then return InfinityWithSign(RightNum.IsInfinity) for
+Infinity and TGocciaNumberLiteralValue.ZeroValue for
RightNum.IsNegativeInfinity; else (Abs(LeftNum.Value) < 1) return
TGocciaNumberLiteralValue.ZeroValue for RightNum.IsInfinity and
InfinityWithSign(RightNum.IsNegativeInfinity) for RightNum.IsNegativeInfinity;
use LeftNum.Value, RightNum.IsInfinite, RightNum.IsInfinity,
RightNum.IsNegativeInfinity,
TGocciaNumberLiteralValue.ZeroValue/OneValue/NaNValue and InfinityWithSign to
implement this logic.

Comment thread units/Goccia.Parser.pas
Comment on lines +1357 to 1479
procedure TGocciaParser.SkipBlock;
var
Depth: Integer;
begin
Consume(gttLeftBrace, 'Expected "{"');
Depth := 1;
while not IsAtEnd and (Depth > 0) do
begin
if Check(gttLeftBrace) then Inc(Depth)
else if Check(gttRightBrace) then Dec(Depth);
if Depth > 0 then Advance;
end;
Consume(gttRightBrace, 'Expected "}"');
end;

procedure TGocciaParser.SkipStatementOrBlock;
begin
if Check(gttLeftBrace) then
SkipBlock
else
SkipUntilSemicolon;
end;

function TGocciaParser.VarStatement: TGocciaStatement;
var
Line, Column: Integer;
begin
Line := Previous.Line;
Column := Previous.Column;

AddWarning('''var'' declarations are not supported in GocciaScript',
'Use ''let'' or ''const'' instead',
Line, Column);

SkipUntilSemicolon;

Result := TGocciaEmptyStatement.Create(Line, Column);
end;

function TGocciaParser.ForStatement: TGocciaStatement;
var
Line, Column: Integer;
begin
Line := Previous.Line;
Column := Previous.Column;

// For now, just consume the basic for loop structure and return a simple statement
Consume(gttLeftParen, 'Expected "(" after "for"');
AddWarning('''for'' loops are not supported in GocciaScript',
'Use array methods like .forEach(), .map(), .filter(), or .reduce() instead',
Line, Column);

// Skip everything until the closing parenthesis
Consume(gttLeftParen, 'Expected "(" after "for"');
while not Check(gttRightParen) and not IsAtEnd do
Advance;

Consume(gttRightParen, 'Expected ")" after for clauses');

// Parse body - can be a block or single statement
if Check(gttLeftBrace) then
begin
Advance; // consume the '{'
Result := BlockStatement;
end
else
Result := Statement;
SkipStatementOrBlock;

Result := TGocciaEmptyStatement.Create(Line, Column);
end;

function TGocciaParser.WhileStatement: TGocciaStatement;
var
Condition: TGocciaExpression;
Body: TGocciaStatement;
Line, Column: Integer;
begin
Line := Previous.Line;
Column := Previous.Column;

AddWarning('''while'' loops are not supported in GocciaScript',
'Use array methods like .forEach(), .map(), .filter(), or .reduce() instead',
Line, Column);

Consume(gttLeftParen, 'Expected "(" after "while"');
Condition := Expression;
while not Check(gttRightParen) and not IsAtEnd do
Advance;
Consume(gttRightParen, 'Expected ")" after while condition');

Body := Statement;
SkipStatementOrBlock;

Result := TGocciaWhileStatement.Create(Condition, Body, Line, Column);
Result := TGocciaEmptyStatement.Create(Line, Column);
end;

function TGocciaParser.DoWhileStatement: TGocciaStatement;
var
Body: TGocciaStatement;
Condition: TGocciaExpression;
Line, Column: Integer;
begin
Line := Previous.Line;
Column := Previous.Column;

// Parse body first
Body := Statement;
AddWarning('''do...while'' loops are not supported in GocciaScript',
'Use array methods like .forEach(), .map(), .filter(), or .reduce() instead',
Line, Column);

// Expect 'while' keyword
Consume(gttWhile, 'Expected "while" after do body');
SkipStatementOrBlock;

// Parse condition
Consume(gttWhile, 'Expected "while" after do body');
Consume(gttLeftParen, 'Expected "(" after "while"');
Condition := Expression;
while not Check(gttRightParen) and not IsAtEnd do
Advance;
Consume(gttRightParen, 'Expected ")" after do-while condition');

// Expect semicolon
Consume(gttSemicolon, 'Expected ";" after do-while statement');

Result := TGocciaDoWhileStatement.Create(Body, Condition, Line, Column);
Result := TGocciaEmptyStatement.Create(Line, Column);
end;

function TGocciaParser.WithStatement: TGocciaStatement;
var
Line, Column: Integer;
begin
Line := Previous.Line;
Column := Previous.Column;

AddWarning('The ''with'' statement is not supported in GocciaScript', '',
Line, Column);

Consume(gttLeftParen, 'Expected "(" after "with"');
while not Check(gttRightParen) and not IsAtEnd do
Advance;
Consume(gttRightParen, 'Expected ")" after with expression');

SkipStatementOrBlock;

Result := TGocciaEmptyStatement.Create(Line, Column);
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

Skip logic can desync on nested parens and compound bodies.

Line 1407-1455 and Line 1471-1474 scan only to the first ), so nested parentheses in loop/with conditions can stop early. Line 1372-1378 uses SkipUntilSemicolon for non-block bodies, which over-skips if/else, try, switch, etc., and can mis-parse the remainder of the file when unsupported constructs appear. Consider a balanced-paren skip helper and parsing a single statement body (then freeing it) for reliable recovery.

🛠️ Possible fix
@@
-    procedure SkipBlock;
-    procedure SkipStatementOrBlock;
+    procedure SkipBlock;
+    procedure SkipParenGroup;
+    procedure SkipStatementOrBlock;
@@
+procedure TGocciaParser.SkipParenGroup;
+var
+  Depth: Integer;
+begin
+  Consume(gttLeftParen, 'Expected "("');
+  Depth := 1;
+  while not IsAtEnd and (Depth > 0) do
+  begin
+    if Check(gttLeftParen) then Inc(Depth)
+    else if Check(gttRightParen) then Dec(Depth);
+    if Depth > 0 then Advance;
+  end;
+  Consume(gttRightParen, 'Expected ")"');
+end;
+
 procedure TGocciaParser.SkipStatementOrBlock;
+var
+  Stmt: TGocciaStatement;
 begin
   if Check(gttLeftBrace) then
     SkipBlock
   else
-    SkipUntilSemicolon;
+  begin
+    Stmt := Statement;
+    Stmt.Free;
+  end;
 end;
@@
-  Consume(gttLeftParen, 'Expected "(" after "for"');
-  while not Check(gttRightParen) and not IsAtEnd do
-    Advance;
-  Consume(gttRightParen, 'Expected ")" after for clauses');
+  SkipParenGroup;
@@
-  Consume(gttLeftParen, 'Expected "(" after "while"');
-  while not Check(gttRightParen) and not IsAtEnd do
-    Advance;
-  Consume(gttRightParen, 'Expected ")" after while condition');
+  SkipParenGroup;
@@
-  Consume(gttLeftParen, 'Expected "(" after "while"');
-  while not Check(gttRightParen) and not IsAtEnd do
-    Advance;
-  Consume(gttRightParen, 'Expected ")" after do-while condition');
+  SkipParenGroup;
@@
-  Consume(gttLeftParen, 'Expected "(" after "with"');
-  while not Check(gttRightParen) and not IsAtEnd do
-    Advance;
-  Consume(gttRightParen, 'Expected ")" after with expression');
+  SkipParenGroup;

Also applies to: 1215-1216

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

In `@units/Goccia.Parser.pas` around lines 1357 - 1479, Skip logic in
SkipBlock/SkipStatementOrBlock and in
VarStatement/ForStatement/WhileStatement/DoWhileStatement/WithStatement
currently scans to the first ')' or uses SkipUntilSemicolon which can desync on
nested parentheses and compound bodies; replace the ad-hoc scans by (1) adding a
balanced-paren helper (e.g. SkipBalancedParens) used by
ForStatement/WhileStatement/DoWhileStatement/WithStatement instead of looping
until the first gttRightParen, and (2) change SkipStatementOrBlock to actually
parse a single statement body (call the existing statement parsing routine to
build the statement and then free it) rather than calling SkipUntilSemicolon, so
recovery handles nested blocks, if/else/try/switch correctly. Ensure SkipBlock
remains balanced for braces and use these helpers from the referenced methods to
avoid over-skipping.

Comment thread units/Goccia.Version.pas
Comment on lines +38 to +57
procedure Initialize;
var
Describe: string;
DashPos: SizeInt;
begin
Describe := RunGit(['describe', '--tags', '--always']);
CachedCommit := RunGit(['rev-parse', '--short', 'HEAD']);

if (Describe = '') or not LooksLikeSemver(Describe) then
begin
CachedVersion := '0.0.0-dev';
Exit;
end;

DashPos := Pos('-', Describe);
if DashPos > 0 then
CachedVersion := Copy(Describe, 1, DashPos - 1) + '-dev'
else
CachedVersion := Describe;
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for test references to commit.length and git-related tests
find . -name "*.js" -o -name "*.pas" | head -20
rg -n "commit.length" --type js
rg -n "commit" tests/ -A 3 -B 3 | head -100

Repository: frostney/GocciaScript

Length of output: 1849


🏁 Script executed:

# Check the GetCommit function implementation
rg -n "function GetCommit\|procedure GetCommit" units/Goccia.Version.pas -A 5

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Check RunGit implementation to see what it returns when git is unavailable
rg -n "function RunGit\|procedure RunGit" units/ -A 10

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Search for GetCommit and RunGit without strict function syntax
rg -n "GetCommit\|RunGit" units/Goccia.Version.pas -B 2 -A 5

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Get the full content of Goccia.Version.pas
cat units/Goccia.Version.pas

Repository: frostney/GocciaScript

Length of output: 1280


🏁 Script executed:

# Search for where GocciaScript.commit is set from
rg -n "GocciaScript.*commit\|GetCommit" units/ --type pas -B 2 -A 2

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

# Search for where GetCommit is used to set GocciaScript.commit
rg -n "GetCommit\|GocciaScript.*commit" units/ -B 2 -A 2

Repository: frostney/GocciaScript

Length of output: 47


🏁 Script executed:

# Check for any CI configuration that might run in git environments
ls -la | grep -E "\.yml|\.yaml|github|gitlab|drone"
cat .github/workflows/*.yml 2>/dev/null | head -50

Repository: frostney/GocciaScript

Length of output: 1385


🏁 Script executed:

# Check if there's any handling for non-git environments in tests or build
rg -n "git|commit|version" build.pas -B 2 -A 2

Repository: frostney/GocciaScript

Length of output: 298


Add fallback for empty commit hash in non-git environments.

The test in tests/language/global-properties/gocciascript.js (lines 48–49) expects GocciaScript.commit.length > 0, but when git is unavailable or the directory is not a git repository, CachedCommit will be an empty string while CachedVersion falls back to "0.0.0-dev". This causes the test assertion to fail.

Add a fallback for the commit hash:

Fix empty commit fallback
   if (Describe = '') or not LooksLikeSemver(Describe) then
   begin
     CachedVersion := '0.0.0-dev';
+    if CachedCommit = '' then
+      CachedCommit := 'unknown';
     Exit;
   end;
+
+  if CachedCommit = '' then
+    CachedCommit := 'unknown';
📝 Committable suggestion

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

Suggested change
procedure Initialize;
var
Describe: string;
DashPos: SizeInt;
begin
Describe := RunGit(['describe', '--tags', '--always']);
CachedCommit := RunGit(['rev-parse', '--short', 'HEAD']);
if (Describe = '') or not LooksLikeSemver(Describe) then
begin
CachedVersion := '0.0.0-dev';
Exit;
end;
DashPos := Pos('-', Describe);
if DashPos > 0 then
CachedVersion := Copy(Describe, 1, DashPos - 1) + '-dev'
else
CachedVersion := Describe;
end;
procedure Initialize;
var
Describe: string;
DashPos: SizeInt;
begin
Describe := RunGit(['describe', '--tags', '--always']);
CachedCommit := RunGit(['rev-parse', '--short', 'HEAD']);
if (Describe = '') or not LooksLikeSemver(Describe) then
begin
CachedVersion := '0.0.0-dev';
if CachedCommit = '' then
CachedCommit := 'unknown';
Exit;
end;
if CachedCommit = '' then
CachedCommit := 'unknown';
DashPos := Pos('-', Describe);
if DashPos > 0 then
CachedVersion := Copy(Describe, 1, DashPos - 1) + '-dev'
else
CachedVersion := Describe;
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Version.pas` around lines 38 - 57, In Initialize, after calling
RunGit to set CachedCommit, add a fallback when CachedCommit is empty (non-git
or git unavailable) by assigning a non-empty placeholder (e.g. 'unknown' or
'0000000') so tests expecting GocciaScript.commit.length > 0 pass; update the
procedure that calls RunGit(['rev-parse', '--short', 'HEAD']) and set
CachedCommit to the placeholder if it returns an empty string, leaving the
existing CachedVersion logic unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant