Skip to content

Add wildcard, function, loose equality and label to unsupported features#32

Merged
frostney merged 3 commits into
mainfrom
fix-unused-feature-behaviour
Feb 22, 2026
Merged

Add wildcard, function, loose equality and label to unsupported features#32
frostney merged 3 commits into
mainfrom
fix-unused-feature-behaviour

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 22, 2026

Summary by CodeRabbit

  • New Features

    • Unsupported constructs (function declarations/expressions, loose equality ==/!=, wildcard re-exports, labeled statements) now emit warnings and act as no-ops or produce undefined-like results so execution continues.
  • Documentation

    • Expanded language restrictions with examples, warning guidance, and suggested alternatives for these syntaxes.
  • Tests

    • Added and expanded tests validating wildcard re-exports and the extended unsupported-features behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 22, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds lexer/token and parser recognition for several previously-ignored syntactic forms (function declarations/expressions, loose equality ==/!=, wildcard re-exports, labeled statements). These constructs now emit parser warnings and are treated as no-ops (empty or undefined results); documentation and tests updated accordingly.

Changes

Cohort / File(s) Summary
Documentation & Language Specs
AGENTS.md, docs/language-restrictions.md
Expanded documentation to list and exemplify newly unsupported constructs (function declarations/expressions, ==/!=, export * from ..., labeled statements); added warning examples and contextual notes.
Lexer & Token Definitions
units/Goccia.Lexer.pas, units/Goccia.Token.pas, units/Goccia.Keywords.Reserved.pas
Added function keyword and tokens gttFunction, gttLooseEqual, gttLooseNotEqual; lexer now emits loose-equality tokens instead of raising on lone =/!. Reserved keywords list extended with function.
Parser
units/Goccia.Parser.pas
Introduced FunctionStatement and branches to recognize function statements/expressions, loose equality tokens, wildcard re-exports, and labeled statements; parser emits warnings and returns TGocciaEmptyStatement or undefined placeholders for unsupported forms.
Tests
tests/language/modules/wildcard-re-export.js, tests/language/statements/unsupported-features.js
Added tests asserting parser warnings and no-op behavior for wildcard re-exports, function declarations/expressions, ==/!=, and labeled statements; verifies code after skipped constructs still executes.
Keywords Declaration
units/Goccia.Keywords.Reserved.pas
Added public constant KEYWORD_FUNCTION = 'function'.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • Warning when using unsupported features #15: Directly related—extends unsupported-features warning work and adds tokens/parser handling (gttFunction, gttLooseEqual/gttLooseNotEqual, FunctionStatement, wildcard re-exports, labeled-statement handling).
  • Type annotations as comments #14: Related changes around TGocciaEmptyStatement and evaluator/AST paths for skipped constructs; overlaps on no-op/undefined semantics.
  • Named exports modules #13: Related export/re-export parsing edits; intersects with the wildcard re-export handling added here.

Poem

🐇 I hopped through tokens, left a little thunk,
Functions tucked away, equality's a skunk,
Stars in re-exports fell from the sky,
Labels peeled off as I bounced by,
Warnings whispered — the parser winked, and I hopped on.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes, which add support for treating four previously unsupported language features (wildcard re-exports, function declarations/expressions, loose equality operators, and labeled statements) as no-ops with warnings.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-unused-feature-behaviour

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

@frostney frostney changed the title Add wildcard and label to unsupported features Add wildcard, function, loose equality and label to unsupported features Feb 22, 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.

🧹 Nitpick comments (3)
docs/language-restrictions.md (3)

160-164: Add language specifier to fenced code block.

The warning example code block should specify a language (e.g., text or plaintext) to satisfy markdown linting rules and improve consistency with other documentation.

📝 Suggested fix
-```
+```text
 Warning: 'function' declarations are not supported in GocciaScript
   Suggestion: Use arrow functions instead: const name = (...) => { ... }
   --> script.js:1:1
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/language-restrictions.md around lines 160 - 164, Update the fenced code
block that contains the warning text starting with "Warning: 'function'
declarations are not supported in GocciaScript" to include a language specifier
(for example change totext) so the block becomes text ... ; this
satisfies markdown linting and keeps the example consistent with other docs.


</details>

---

`184-188`: **Add language specifier to fenced code block.**

Similar to the function keyword warning block, this should specify `text` or `plaintext` for consistency.



<details>
<summary>📝 Suggested fix</summary>

```diff
-```
+```text
 Warning: '==' (loose equality) is not supported in GocciaScript
   Suggestion: Use '===' (strict equality) instead
   --> script.js:1:10
 ```
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/language-restrictions.md around lines 184 - 188, Update the fenced code
block that shows the warning about '==' to include a language specifier (e.g.,
change the opening fence from totext or ```plaintext) so it matches the
other examples like the "function keyword warning" block; locate the block
containing the lines "Warning: '==' (loose equality) is not supported in
GocciaScript" and add the language token to the opening fence for consistency.


</details>

---

`270-273`: **Add language specifier to fenced code block.**

The labeled statement warning example should also specify a language.



<details>
<summary>📝 Suggested fix</summary>

```diff
-```
+```text
 Warning: Labeled statements are not supported in GocciaScript
   --> script.js:1:1
 ```
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/language-restrictions.md around lines 270 - 273, Update the fenced code
block containing the warning text "Warning: Labeled statements are not supported
in GocciaScript --> script.js:1:1" to include a language specifier (e.g.,

docs/language-restrictions.md file around the example and change the opening
fence from ``` to ```text so the warning example is explicitly marked as plain
text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/language-restrictions.md`:
- Around line 160-164: Update the fenced code block that contains the warning
text starting with "Warning: 'function' declarations are not supported in
GocciaScript" to include a language specifier (for example change ``` to
```text) so the block becomes ```text ... ```; this satisfies markdown linting
and keeps the example consistent with other docs.
- Around line 184-188: Update the fenced code block that shows the warning about
'==' to include a language specifier (e.g., change the opening fence from ``` to
```text or ```plaintext) so it matches the other examples like the "function
keyword warning" block; locate the block containing the lines "Warning: '=='
(loose equality) is not supported in GocciaScript" and add the language token to
the opening fence for consistency.
- Around line 270-273: Update the fenced code block containing the warning text
"Warning: Labeled statements are not supported in GocciaScript -->
script.js:1:1" to include a language specifier (e.g., ```text) for proper
rendering; locate the block in the docs/language-restrictions.md file around the
example and change the opening fence from ``` to ```text so the warning example
is explicitly marked as plain text.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 22, 2026

Benchmark Results

179 benchmarks · 🟢 92 improved · 87 unchanged · avg +9.8%

arrays.js — 🟢 1 improved, 18 unchanged · avg +4.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 13,802 13,991 +1.4%
Array.from 10 elements 159,861 160,953 +0.7%
Array.of 10 elements 195,467 196,890 +0.7%
spread into new array 228,329 241,640 +5.8%
map over 50 elements 11,234 11,567 +3.0%
filter over 50 elements 9,616 9,922 +3.2%
reduce sum 50 elements 12,086 12,430 +2.8%
forEach over 50 elements 9,705 9,961 +2.6%
find in 50 elements 12,932 13,212 +2.2%
sort 20 elements 10,115 10,509 +3.9%
flat nested array 87,351 91,747 +5.0%
flatMap 56,757 60,734 🟢 +7.0%
map inside map (5x5) 16,243 17,039 +4.9%
filter inside map (5x10) 5,233 5,526 +5.6%
reduce inside map (5x10) 6,109 6,458 +5.7%
forEach inside forEach (5x10) 5,588 5,789 +3.6%
find inside some (10x10) 3,425 3,588 +4.7%
map+filter chain nested (5x20) 2,268 2,411 +6.3%
reduce flatten (10x5) 6,025 6,438 +6.9%
classes.js — 15 unchanged · avg +2.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 101,926 103,545 +1.6%
class with defaults 78,934 81,393 +3.1%
50 instances via Array.from 4,758 4,904 +3.1%
instance method call 46,845 48,339 +3.2%
static method call 81,364 83,109 +2.1%
single-level inheritance 39,853 41,164 +3.3%
two-level inheritance 35,192 36,463 +3.6%
private field access 46,254 47,535 +2.8%
private methods 54,383 55,188 +1.5%
getter/setter access 54,081 55,494 +2.6%
static getter read 97,514 100,736 +3.3%
static getter/setter pair 69,266 70,632 +2.0%
inherited static getter 54,323 56,288 +3.6%
inherited static setter 57,842 59,589 +3.0%
inherited static getter with this binding 46,401 48,139 +3.7%
closures.js — 11 unchanged · avg +5.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 84,249 87,887 +4.3%
closure over multiple variables 95,712 101,181 +5.7%
nested closures 106,141 111,446 +5.0%
function as argument 75,531 79,833 +5.7%
function returning function 95,756 101,367 +5.9%
compose two functions 58,401 61,695 +5.6%
fn.call 137,226 144,177 +5.1%
fn.apply 91,058 95,370 +4.7%
fn.bind 114,025 120,223 +5.4%
recursive sum to 50 7,955 8,429 +6.0%
recursive tree traversal 13,908 14,720 +5.8%
collections.js — 🟢 5 improved, 7 unchanged · avg +7.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 5,736 6,092 +6.2%
has lookup (50 elements) 8,924 9,517 +6.6%
delete elements 25,450 27,082 +6.4%
forEach iteration 5,671 5,998 +5.8%
spread to array 12,716 13,518 +6.3%
deduplicate array 40,199 42,339 +5.3%
set 50 entries 4,350 4,646 +6.8%
get lookup (50 entries) 4,468 4,792 🟢 +7.3%
has check 4,507 4,955 🟢 +9.9%
delete entries 12,107 13,180 🟢 +8.9%
forEach iteration 3,433 3,764 🟢 +9.6%
keys/values/entries 4,468 4,820 🟢 +7.9%
destructuring.js — 🟢 1 improved, 13 unchanged · avg +5.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 284,456 300,363 +5.6%
with rest element 182,295 192,681 +5.7%
with defaults 287,813 305,356 +6.1%
skip elements 291,116 307,230 +5.5%
nested array destructuring 144,012 151,953 +5.5%
swap variables 348,004 367,910 +5.7%
simple object destructuring 243,936 255,860 +4.9%
with defaults 280,405 298,708 +6.5%
with renaming 287,329 298,843 +4.0%
nested object destructuring 147,081 151,290 +2.9%
rest properties 160,896 164,334 +2.1%
object parameter 85,055 89,298 +5.0%
array parameter 97,684 105,286 🟢 +7.8%
mixed destructuring in map 11,854 12,627 +6.5%
fibonacci.js — 🟢 1 improved, 5 unchanged · avg +5.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 216 229 +5.7%
recursive fib(20) 20 20 +4.1%
iterative fib(20) via reduce 9,542 10,228 🟢 +7.2%
iterator fib(20) 7,244 7,470 +3.1%
iterator fib(20) via Iterator.from + take 8,184 8,541 +4.4%
iterator fib(20) last value via reduce 6,871 7,258 +5.6%
iterators.js — 🟢 10 improved, 10 unchanged · avg +6.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
Iterator.from({next}).toArray() — 20 elements 9,209 9,621 +4.5%
Iterator.from({next}).toArray() — 50 elements 3,926 4,062 +3.5%
spread pre-wrapped iterator — 20 elements 9,130 9,540 +4.5%
Iterator.from({next}).forEach — 50 elements 3,015 3,120 +3.5%
Iterator.from({next}).reduce — 50 elements 3,044 3,256 +7.0%
wrap array iterator 61,272 63,924 +4.3%
wrap plain {next()} object 6,279 6,557 +4.4%
map + toArray (50 elements) 2,677 2,808 +4.9%
filter + toArray (50 elements) 2,700 2,894 🟢 +7.2%
take(10) + toArray (50 element source) 14,962 15,793 +5.6%
drop(40) + toArray (50 element source) 3,758 4,067 🟢 +8.2%
chained map + filter + take (100 element source) 4,716 5,099 🟢 +8.1%
some + every (50 elements) 1,692 1,842 🟢 +8.9%
find (50 elements) 3,660 3,970 🟢 +8.5%
array.values().map().filter().toArray() 3,273 3,556 🟢 +8.6%
array.values().take(5).toArray() 17,943 19,301 🟢 +7.6%
array.values().drop(45).toArray() 11,146 11,796 +5.8%
map.entries() chained helpers 3,497 3,803 🟢 +8.7%
set.values() chained helpers 5,910 6,423 🟢 +8.7%
string iterator map + toArray 8,152 8,943 🟢 +9.7%
json.js — 🟢 14 improved, 6 unchanged · avg +8.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 167,426 174,098 +4.0%
parse nested object 108,991 112,773 +3.5%
parse array of objects 58,805 60,288 +2.5%
parse large flat object 50,748 50,416 -0.7%
parse mixed types 72,869 74,513 +2.3%
stringify simple object 142,598 153,055 🟢 +7.3%
stringify nested object 79,000 86,293 🟢 +9.2%
stringify array of objects 16,610 17,921 🟢 +7.9%
stringify mixed types 63,243 69,901 🟢 +10.5%
reviver doubles numbers 34,358 37,126 🟢 +8.1%
reviver filters properties 31,046 33,495 🟢 +7.9%
reviver on nested object 40,098 44,183 🟢 +10.2%
reviver on array 21,210 22,382 +5.5%
replacer function doubles numbers 32,595 36,414 🟢 +11.7%
replacer function excludes properties 43,715 49,735 🟢 +13.8%
array replacer (allowlist) 86,914 100,279 🟢 +15.4%
stringify with 2-space indent 69,046 77,648 🟢 +12.5%
stringify with tab indent 69,304 79,571 🟢 +14.8%
parse then stringify 43,765 47,437 🟢 +8.4%
stringify then parse 16,664 18,202 🟢 +9.2%
jsx.jsx — 🟢 21 improved · avg +19.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple element 182,531 204,492 🟢 +12.0%
self-closing element 182,665 210,015 🟢 +15.0%
element with string attribute 149,526 175,525 🟢 +17.4%
element with multiple attributes 123,890 147,023 🟢 +18.7%
element with expression attribute 128,996 155,656 🟢 +20.7%
text child 167,759 203,175 🟢 +21.1%
expression child 161,241 197,573 🟢 +22.5%
mixed text and expression 152,088 184,123 🟢 +21.1%
nested elements (3 levels) 61,319 75,757 🟢 +23.5%
sibling children 45,775 56,265 🟢 +22.9%
component element 123,619 142,415 🟢 +15.2%
component with children 70,355 88,496 🟢 +25.8%
dotted component 98,848 118,655 🟢 +20.0%
empty fragment 169,757 207,382 🟢 +22.2%
fragment with children 47,555 55,823 🟢 +17.4%
spread attributes 91,441 104,513 🟢 +14.3%
spread with overrides 80,392 87,239 🟢 +8.5%
shorthand props 123,950 145,821 🟢 +17.6%
nav bar structure 21,684 26,505 🟢 +22.2%
card component tree 25,295 30,545 🟢 +20.8%
10 list items via Array.from 11,630 14,141 🟢 +21.6%
numbers.js — 🟢 10 improved, 1 unchanged · avg +11.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 301,466 349,631 🟢 +16.0%
floating point arithmetic 311,112 369,379 🟢 +18.7%
number coercion 141,516 156,085 🟢 +10.3%
toFixed 84,638 92,767 🟢 +9.6%
toString 120,483 137,763 🟢 +14.3%
valueOf 175,851 184,591 +5.0%
toPrecision 110,912 127,868 🟢 +15.3%
Number.isNaN 223,629 244,687 🟢 +9.4%
Number.isFinite 220,946 243,367 🟢 +10.1%
Number.isInteger 203,233 224,767 🟢 +10.6%
Number.parseInt and parseFloat 180,527 198,967 🟢 +10.2%
objects.js — 🟢 7 improved · avg +19.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 327,014 388,408 🟢 +18.8%
create nested object 164,557 205,194 🟢 +24.7%
create 50 objects via Array.from 8,058 9,873 🟢 +22.5%
property read 159,356 182,986 🟢 +14.8%
Object.keys 116,043 126,506 🟢 +9.0%
Object.entries 61,471 75,527 🟢 +22.9%
spread operator 134,825 164,726 🟢 +22.2%
promises.js — 🟢 12 improved · avg +23.5%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 377,405 439,201 🟢 +16.4%
new Promise(resolve => resolve(value)) 136,283 165,665 🟢 +21.6%
Promise.reject(reason) 399,981 469,007 🟢 +17.3%
resolve + then (1 handler) 116,304 148,214 🟢 +27.4%
resolve + then chain (3 deep) 48,882 60,966 🟢 +24.7%
resolve + then chain (10 deep) 15,371 20,203 🟢 +31.4%
reject + catch + then 71,635 90,129 🟢 +25.8%
resolve + finally + then 60,714 74,424 🟢 +22.6%
Promise.all (5 resolved) 23,032 29,521 🟢 +28.2%
Promise.race (5 resolved) 24,877 31,814 🟢 +27.9%
Promise.allSettled (5 mixed) 21,819 25,727 🟢 +17.9%
Promise.any (5 mixed) 24,221 29,365 🟢 +21.2%
strings.js — 🟢 10 improved, 1 unchanged · avg +12.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 338,497 378,755 🟢 +11.9%
template literal 371,561 403,709 🟢 +8.7%
string repeat 336,152 371,766 🟢 +10.6%
split and join 115,395 133,353 🟢 +15.6%
indexOf and includes 136,362 149,537 🟢 +9.7%
toUpperCase and toLowerCase 204,634 240,151 🟢 +17.4%
slice and substring 117,241 132,890 🟢 +13.3%
trim operations 152,868 175,843 🟢 +15.0%
replace and replaceAll 170,399 192,619 🟢 +13.0%
startsWith and endsWith 115,848 123,377 +6.5%
padStart and padEnd 157,964 179,385 🟢 +13.6%

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.

🧹 Nitpick comments (1)
docs/language-restrictions.md (1)

182-190: Clarify whether operands are evaluated at runtime.

Line 190 states "Both operands are parsed but discarded." The phrase "parsed but discarded" is ambiguous—it's unclear whether the operands are evaluated at runtime or skipped entirely.

This matters for side effects:

let x = 0;
if (x++ == x++) { }  // Are the increments executed?
if (foo() == bar()) { }  // Are foo() and bar() called?

Consider clarifying whether:

  1. Operands are not evaluated at runtime (skipped entirely), or
  2. Operands are evaluated (side effects happen) but the comparison result is discarded
📝 Suggested clarification

Replace line 190 with one of:

If operands are NOT evaluated:

Both operands are parsed but not evaluated at runtime (no side effects). Because the expression evaluates to `undefined` (which is falsy), `==`/`!=` in conditions (e.g., `if (a == b)`) will never enter the truthy branch.

If operands ARE evaluated:

Both operands are evaluated (side effects occur) but the comparison result is discarded. The expression evaluates to `undefined`. Because `undefined` is falsy, `==`/`!=` in conditions (e.g., `if (a == b)`) will never enter the truthy branch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/language-restrictions.md` around lines 182 - 190, The sentence "Both
operands are parsed but discarded." in the paragraph about `==`/`!=` is
ambiguous about side effects—update that sentence to explicitly state whether
the operand expressions are evaluated at runtime or not; replace it with one of
the two suggested clarifications from the reviewer: either "Both operands are
parsed but not evaluated at runtime (no side effects)...", or "Both operands are
evaluated (side effects occur) but the comparison result is discarded..."; keep
the surrounding examples (`if (a == b)`, `if (x++ == x++)`, `if (foo() ==
bar())`) intact so readers can see the semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/language-restrictions.md`:
- Around line 182-190: The sentence "Both operands are parsed but discarded." in
the paragraph about `==`/`!=` is ambiguous about side effects—update that
sentence to explicitly state whether the operand expressions are evaluated at
runtime or not; replace it with one of the two suggested clarifications from the
reviewer: either "Both operands are parsed but not evaluated at runtime (no side
effects)...", or "Both operands are evaluated (side effects occur) but the
comparison result is discarded..."; keep the surrounding examples (`if (a ==
b)`, `if (x++ == x++)`, `if (foo() == bar())`) intact so readers can see the
semantics.

@frostney frostney merged commit 83f4217 into main Feb 22, 2026
4 checks passed
@frostney frostney deleted the fix-unused-feature-behaviour branch February 22, 2026 21:41
@frostney frostney added the new feature New feature or request label Apr 9, 2026
frostney added a commit that referenced this pull request May 5, 2026
ES2026 §11.2 WhiteSpace and §11.3 LineTerminator include Unicode
characters beyond ASCII space — Tab/LF/CR/VT/FF/SP/NBSP/USP plus the
ZWNBSP and LS/PS line terminators. parseInt/parseFloat call
TrimString(string, start) which uses StrWhiteSpace, so a Unicode
whitespace prefix like `\u3000` (ideographic space) must be skipped
before scanning digits.

Goccia's parseInt/parseFloat were calling FreePascal's `Trim()`, which
only strips ASCII whitespace (#0..#32). `parseInt("\u20001")` returned
NaN instead of 1. Switch both to `TrimECMAScriptWhitespace` from
`TextSemantics`, the same helper String.prototype.trim and BigInt
parsing already use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frostney added a commit that referenced this pull request May 6, 2026
* Add --compat-traditional-for-loop opt-in flag

Adds traditional C-style for(init; test; update) loops behind a new
compatibility flag, mirroring the existing --compat-var / --compat-function
posture. let/const declarations in for-init create per-iteration lexical
environments per ES2026 §14.7.4.4 so closure capture pins per iteration.
var declarations require both --compat-var and the new flag, hoist out of
the loop, and share a single binding. Bytecode mode includes a counted-loop
fast path mirroring CompileCountedForOf for for(let i = N; i <op> M; i++|i--)
shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Wire --compat-traditional-for-loop through GocciaScriptLoaderBare

The bare loader has its own option-parsing and engine-config path,
so the flag has to be threaded through it explicitly (mirrors the
existing --compat-var / --compat-function plumbing). Without this,
test262 (which runs through the bare loader with --compat-all) was
not picking up the flag and only one extra traditional-for test passed
across the suite. With the wiring, language/statements/for/ goes from
60/385 to 325/385 in bytecode mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address PR review: fast-path safety, const update, interpolation parser

- TryCompileCountedFor now bails when the init has a type annotation or
  the condition RHS isn't a literal/identifier the body doesn't reassign;
  the spec re-evaluates the test each iteration, so snapshotting a
  side-effecting or mutable RHS once is wrong (#531 review).
- EvaluateFor's iteration→header sync now uses ForceUpdateBinding so a
  body that runs to completion under `for (const ...)` doesn't trip the
  evaluator's own writability check on the next iteration's snapshot.
- ParseInterpolationExpression threads TraditionalForLoopsEnabled into
  child parsers so an IIFE inside a template `${...}` can use traditional
  for when the surrounding source has it enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Gate ParseTraditionalForBody on top-level ';' in for-header

Fixes a regression caused by my own dispatch — `for (lhs of expr)` heads
where `lhs` is an assignment target (not a declaration) were previously
warned-and-skipped, but now fell through to ParseTraditionalForBody and
syntax-errored on `Expected ';'` before the `of`. Same shape:
`for ([a, b] of arr)`, `for ({x} of objs)`, etc. Several test262
suites use these forms in harness/feature tests.

LooksLikeTraditionalForHeader is a pure peek that returns True only when
a top-depth ';' exists inside the for-header parens. for-of/for-in
heads contain no top-level ';', so they fall back to the
warn-and-skip path. Also handles the edge case in
language/statements/for/head-init-async-of.js where `for (async of => {};
...; ...)` is a traditional for whose init is an arrow with parameter
named `of` — the prior heuristic would have misclassified that as a
for-of shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Decouple --compat-traditional-for-loop from --compat-all

Bundling traditional `for(;;)` into --compat-all re-runs loop bodies
that warn-and-skipped on baseline, surfacing 367 unrelated engine gaps
(Atomics, Intl, Math.pow edge cases, RegExp tests) that were trivially
passing before. Test262 runs --compat-all unconditionally through the
bare loader, so the bundling forced a -31 net merge-blocking regression.

Make the flag strictly opt-in: callers who want traditional `for(;;)`
must pass --compat-traditional-for-loop explicitly (or set the
config / engine property). --compat-all continues to OR in --compat-var
and --compat-function as before. Test262 baseline is restored.

Also tighten TryCompileCountedFor's condition-RHS guard to literals
only (#531 review): `ForBodyAssignsIdentifier` doesn't see writes
through IIFEs/callbacks/property setters, so `i < limit` was unsafe to
fast-path even when the body looked clean. Falls through to the
general path where the condition is re-evaluated each iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "Decouple --compat-traditional-for-loop from --compat-all"

This reverts commit cd0f7bb.

* Re-tighten counted-for fast path RHS to literal-only

Brings the bot review fix forward over the decouple revert. The
identifier-RHS branch in TryCompileCountedFor used
ForBodyAssignsIdentifier as a safety check, but that walker doesn't
see writes through IIFEs, callbacks, property setters, or eval-like
reaches — so a bare-identifier RHS was unsafe for `i < limit` shapes
even when the body looked clean. Falls through to the general path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Implement Math.pow & parseFloat per ES2026 spec

Both bugs were silently passing test262 because traditional for(;;)
loops in their test bodies warn-and-skipped on baseline. Restoring
--compat-all coverage in #531 surfaced both as honest regressions;
fix at the source.

Math.pow (§6.1.6.1.3 Number::exponentiate)
  Previous impl forwarded directly to FreePascal's `Power(B, E)`,
  which uses `exp(E * ln(B))` and returns NaN for any base ≤ 0. Spec
  has 30+ special cases: NaN/+0/-0 exponent, ±∞ base, ±∞ exponent,
  signed-zero/Infinity sign rules driven by "exponent is an odd
  integer". Adds explicit branches for §6.1.6.1.3 steps 1-13 and
  guards Round() against doubles above 2^53 (oddness can only be
  detected up to that magnitude — Round(1.8e308) overflows Int64).
  Fixes Math/pow/* tests for ±Infinity^x and x^±Infinity.

parseFloat (§21.1.2.13.1 StrUnsignedDecimalLiteral)
  Previous impl parsed integer + fractional digits but no
  ExponentPart, so `parseFloat("0.1e1")` returned 0.1 instead of 1.
  Adds optional `(e|E)[+-]?DecimalDigits` parsing after the fraction;
  malformed exponent (stray 'e' with no digits) returns the mantissa
  alone per the longest-valid-prefix rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* parseInt/parseFloat trim ECMAScript whitespace, not just ASCII

ES2026 §11.2 WhiteSpace and §11.3 LineTerminator include Unicode
characters beyond ASCII space — Tab/LF/CR/VT/FF/SP/NBSP/USP plus the
ZWNBSP and LS/PS line terminators. parseInt/parseFloat call
TrimString(string, start) which uses StrWhiteSpace, so a Unicode
whitespace prefix like `\u3000` (ideographic space) must be skipped
before scanning digits.

Goccia's parseInt/parseFloat were calling FreePascal's `Trim()`, which
only strips ASCII whitespace (#0..#32). `parseInt("\u20001")` returned
NaN instead of 1. Switch both to `TrimECMAScriptWhitespace` from
`TextSemantics`, the same helper String.prototype.trim and BigInt
parsing already use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Number.prototype.toString radix + Math.ceil signed-zero per spec

Number.prototype.toString (§21.1.3.6 / §6.1.6.1.20)
  Previous impl only handled radix 10 (default ToString) and radix 16
  (IntToHex two's-complement, which gave `(-255).toString(16) =
  'ffffffffffffff01'` instead of `-ff`). Every radix in 11-15, 17-36
  fell through to decimal output. Implements the generalized
  decimal-to-radix algorithm: optional sign + integer-part-digits +
  optional `.fraction-digits`, using `0-9a-z` for digits 10-35.
  IntegerToRadixString and FractionToRadixString are file-local helpers
  shared with the integer path; the fraction conversion uses 52
  digits (enough to round-trip a binary64 mantissa) with trailing-zero
  trimming. Fixes Number/prototype/toString/a-z and any test using
  non-10 / non-16 radix.

Math.ceil (§21.3.2.10)
  Per spec step 3, when the argument is in (-1, 0) the result is -0𝔽
  (preserves signed-zero). Goccia returned `+0` because it skipped
  straight to FreePascal's Ceil(), which collapses signed zeros. Adds
  the explicit "negative-fractional → -0" branch so
  `Math.ceil(-0.1) === -Math.floor(0.1)` per Object.is. Fixes
  Math/ceil/S15.8.2.6_A7 (the 2000-iteration ceil(x) ≡ -floor(-x)
  identity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Math.floor preserves signed zero per spec

ES2026 §21.3.2.16: Math.floor(-0𝔽) is -0𝔽, not +0𝔽. Goccia was passing
through to FreePascal's `Floor()` which collapses signed zeros.
Returns the argument directly when it is -0 or +0 so the
ceil(x) ≡ -floor(-x) identity (Math.floor/S15.8.2.6_A7) holds at the
zero boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Array.{sort,toSorted}: validate comparator type/order per spec

Both methods had two divergences from ES2026 §23.1.3.{29,34}:

1. Validation order: spec step 1 throws TypeError when comparefn is
   non-undefined and non-callable, BEFORE LengthOfArrayLike reads
   `this.length`. Goccia read length first, so a getter that throws on
   length was observed even when the comparator argument was already
   bad. test262 specifically covers this with a `getLengthThrow` object
   plus invalidComparators list (null/true/false/""/regex/number/
   bigint/[]/{}/Symbol).

2. Error type: ThrowError was throwing a generic Error rather than
   TypeError, so `assert.throws(TypeError, ...)` saw the wrong
   constructor. Switch to ThrowTypeError to match the spec's
   "throw a TypeError exception".

Also pull comparator extraction up so the undefined-comparator branch
falls through to the default ascending compare without re-checking
AArgs.Length downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* parseFloat: use division for negative exponents (FP precision)

`parseFloat("0.1e-1")` returned `0.010000000000000002` instead of
`0.01`. Multiplying 0.1 by IntPower(10, -1) = 0.1 yields the imprecise
product (0.1 * 0.1 has FP error). Dividing by IntPower(10, 1) = 10
lands exactly on the closest IEEE 754 double to 0.01.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR review: tighten counted fast path + fix Number.toString edge cases

- TryCompileCountedFor now also rejects non-integer literal RHS so
  `for(let i=0; i<3.5; i++)` doesn't take the OP_GTE_INT/OP_LTE_INT
  fast path; falls through to the general path which preserves IEEE
  754 compare semantics. (#531 review)
- IntegerToRadixString takes a Double, so values above Int64.MaxValue
  (which still produce a finite Number) no longer overflow on
  Round() and stringify via repeated div/mod in floating-point. The
  trailing-bit alignment for values past 2^53 is implementation-defined
  per ES2026 §6.1.6.1.20.
- FractionToRadixString sizes the digit budget per radix as
  `Ceil(53 / Log2(R)) + 4` so binary64-precision values capture their
  mantissa in any base 2..36 (the previous fixed 52-digit cap was too
  short for radix 2 and overshot for radix 36). Full subnormal range
  remains future work — a proper Steele-White / Grisu implementation
  is tracked separately.
- docs/language.md: clarify `continue` only applies to iteration
  constructs, never `switch`.
- docs/decision-log.md: spell out that `--compat-traditional-for-loop`
  is bundled into `--compat-all` and reference the engine-gap issues
  surfaced (#540 / #541 / #542).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR review: Int() in Number.toString + tighter no-op doc wording

- IntegerToRadixString and the call site at NumberToString now use
  Int() instead of Trunc(). Trunc() returns Int64 and tripped a
  range-check error for any double past 2^63 (e.g.
  (1e100).toString(16)). Int() returns the truncated value as Double
  so arbitrarily large finite numbers stringify; Round() narrows just
  the small modulo digit. (#531 review)
- docs/language.md: replace "loops" in the Graceful Handling bullet
  with the precise excluded list — `while`/`do...while` always, and
  traditional `for(;;)` only when --compat-traditional-for-loop is
  off. The previous wording implied all loops are no-op which now
  conflicts with opt-in traditional-for support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR review: align downstream "traditional loops excluded" wording

Two follow-on doc consistencies after opt-in traditional `for(;;)`:

- docs/language.md § Labeled Statements: "Since GocciaScript excludes
  traditional loops, labels serve no purpose" was outdated — traditional
  for(;;) is now available behind --compat-traditional-for-loop.
  Rewrite to call out which loop forms exist and that labeled
  break/continue isn't implemented for any of them.
- docs/language-tables.md while/do...while row: list traditional
  for(;;) (with --compat-traditional-for-loop) alongside for-of and
  array methods as the suggested alternatives, matching the prose in
  language.md § while and do...while.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move for-loop var tests to sibling dir for explicit separation

`tests/language/for-loop/with-var/` was a nested subdir; the goccia.json
inside it correctly only applied to its own tests, so isolation was
empirically fine. But the layout made it look like compat-var was a
sub-concern of for-loop, when it's really an orthogonal flag combination.
Move to a sibling at `tests/language/for-loop-var/` so the directory tree
makes the concern split obvious:

  tests/language/for-loop/        — only --compat-traditional-for-loop
                                    (let/const tests, default features)
  tests/language/for-loop-var/    — --compat-traditional-for-loop +
                                    --compat-var (combination tests)

Each dir's goccia.json controls its own scope; running either, both,
or neither still gives the same per-test config.
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