From c12815abaca56b48b9485b8d57eec7aa911bf8d8 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Tue, 2 Jun 2026 08:19:34 +0200 Subject: [PATCH 1/3] Add for-in compatibility flag --- README.md | 2 +- docs/build-system.md | 1 + docs/decision-log.md | 4 +- docs/embedding.md | 1 + docs/errors.md | 1 + docs/goals.md | 2 +- docs/language-tables.md | 1 + docs/language.md | 33 +- docs/test262.md | 13 +- docs/tutorial.md | 1 + scripts/run_test262_suite.ts | 15 +- scripts/test-cli.ts | 7 +- scripts/test262_compatibility_roadmap.json | 1349 ----------------- source/app/Goccia.CLI.Options.pas | 4 +- source/units/Goccia.AST.BindingPatterns.pas | 13 + source/units/Goccia.AST.Statements.pas | 48 + source/units/Goccia.Bytecode.OpCodeNames.pas | 2 + source/units/Goccia.Bytecode.pas | 9 +- source/units/Goccia.Compiler.Statements.pas | 105 ++ source/units/Goccia.Compiler.pas | 16 + source/units/Goccia.Evaluator.pas | 264 +++- source/units/Goccia.Parser.pas | 75 + source/units/Goccia.SourcePipeline.pas | 3 +- source/units/Goccia.VM.pas | 89 ++ source/units/Goccia.Values.ObjectValue.pas | 43 + .../language/for-in-loop/basic-enumeration.js | 114 ++ tests/language/for-in-loop/control-flow.js | 30 + tests/language/for-in-loop/goccia.json | 3 + tests/language/for-in-loop/var/goccia.json | 4 + .../for-in-loop/var/var-shared-binding.js | 72 + .../statements/unsupported-features.js | 14 + 31 files changed, 947 insertions(+), 1391 deletions(-) delete mode 100644 scripts/test262_compatibility_roadmap.json create mode 100644 tests/language/for-in-loop/basic-enumeration.js create mode 100644 tests/language/for-in-loop/control-flow.js create mode 100644 tests/language/for-in-loop/goccia.json create mode 100644 tests/language/for-in-loop/var/goccia.json create mode 100644 tests/language/for-in-loop/var/var-shared-binding.js diff --git a/README.md b/README.md index 8f0adba2..b8bff863 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It's based on the thought "What if we implement ECMAScript today, but without th ## Features -GocciaScript implements a modern subset of ECMAScript: `let`/`const`, arrow functions, classes with private fields, `for...of`, async/await, ES modules, decorators, and TypeScript-style type annotations. Features that are error-prone, redundant, or security risks (`var`, `function` keyword, `==`/`!=`, `eval`, labels, traditional loops) are excluded by default; selected legacy forms are available through explicit compatibility flags. +GocciaScript implements a modern subset of ECMAScript: `let`/`const`, arrow functions, classes with private fields, `for...of`, async/await, ES modules, decorators, and TypeScript-style type annotations. Features that are error-prone, redundant, or security risks (`var`, `function` keyword, `==`/`!=`, `eval`, labels, traditional loops, `for...in`) are excluded by default; selected legacy forms are available through explicit compatibility flags. See [Language](docs/language.md) for the complete specification of supported features, TC39 proposals, and exclusions. diff --git a/docs/build-system.md b/docs/build-system.md index 3ba68f51..29056573 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -240,6 +240,7 @@ Relative paths are resolved against the current working directory. A missing fil "compat-loose-equality": true, "compat-label": true, "compat-traditional-for-loop": true, + "compat-for-in-loop": true, "compat-while-loops": true, "strict-types": true, "unsafe-ffi": true, diff --git a/docs/decision-log.md b/docs/decision-log.md index c7cbec08..69780f88 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -17,6 +17,8 @@ Chronological record of key architectural and implementation decisions, newest f --- +**2026-06-02** · `engine` — Added opt-in `for...in` JavaScript compatibility behind `--compat-for-in-loop` / `"compat-for-in-loop"` / `cfForIn`. The flag remains separate from `--compat-traditional-for-loop` because `for...in` uses property-name enumeration rather than counted-loop control flow. It stays off by default with the existing parser warning/no-op posture and supports enumerable string own and inherited keys in interpreter and bytecode modes. [language.md](language.md#forin-loop). + **2026-05-29** · `engine` — Labeled `break` and `continue` targets are available behind `--compat-label` / `"compat-label"` for JavaScript compatibility and test262 coverage. Labels stay disabled by default, but when enabled the parser preserves label targets and both interpreter and bytecode paths resolve named breaks and iteration-only continues. [language.md § Labeled Statements](language.md#labeled-statements). **2026-05-24** · `parser` — Parser policy now lives behind the source pipeline instead of being repeated at host call sites. `TGocciaSourcePipeline` owns compatibility/source-type mapping and exposes narrow parse entry points for full source, module source, dynamic `Function` validation/wrapping, and expression fragments; module loading consumes a module-specific result rather than the full host-facing parse report. [architecture.md § Overview](architecture.md#overview). [embedding.md § Engine API](embedding.md#engine-api). @@ -43,7 +45,7 @@ Chronological record of key architectural and implementation decisions, newest f **2026-05-11** · `engine` — Added opt-in loose equality compatibility (`--compat-loose-equality` / `"compat-loose-equality"`). `==` and `!=` remain warning-producing no-ops by default, but when enabled they use the shared ES2026 `IsLooselyEqual` implementation in interpreter and bytecode modes, including string/number/boolean coercion, BigInt mixed comparisons, and object `ToPrimitive`; the bytecode format is bumped to v27 for `OP_LOOSE_EQ` and `OP_LOOSE_NEQ`. [language.md](language.md#loose-equality--and-). -**2026-05-10** · `engine` — Removed the `--compat-all` meta-flag. It was originally added to give test262 a single switch that turns on every `--compat-*` option, but in practice it (a) hides which compatibility surface a given run actually depends on, (b) silently widens its own meaning whenever a new `--compat-*` flag lands, and (c) needs the same fan-out logic kept in sync in three frontends (`GocciaScriptLoaderBare.dpr`, `Goccia.CLI.Application.pas`, `GocciaBundler.dpr`). Consumers now enumerate the specific flags they need; `scripts/run_test262_suite.ts` lists `--compat-var --compat-function --compat-traditional-for-loop` explicitly. The individual compat flags themselves (`--compat-var`, `--compat-function`, `--compat-traditional-for-loop`) and their `goccia.json` keys are unchanged. +**2026-05-10** · `engine` — Removed the `--compat-all` meta-flag. It was originally added to give test262 a single switch that turns on every `--compat-*` option, but in practice it (a) hides which compatibility surface a given run actually depends on, (b) silently widens its own meaning whenever a new `--compat-*` flag lands, and (c) needs the same fan-out logic kept in sync in three frontends (`GocciaScriptLoaderBare.dpr`, `Goccia.CLI.Application.pas`, `GocciaBundler.dpr`). Consumers now enumerate the specific flags they need; `scripts/run_test262_suite.ts` lists the required `--compat-*` flags explicitly for the conformance corpus. The individual compat flags themselves and their `goccia.json` keys are unchanged. **2026-05-08** · `runtime` — ECMA-402 Intl namespace. Full implementation of the Internationalization API: Intl.Locale, Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat, Intl.PluralRules, Intl.RelativeTimeFormat, Intl.ListFormat, Intl.DisplayNames, Intl.Segmenter, Intl.DurationFormat, plus Intl.getCanonicalLocales and Intl.supportedValuesOf. Data strategy mirrors Temporal timezone: system ICU library (macOS libicucore, Linux libicui18n, Windows icu.dll shared with Temporal) as primary, embedded CLDR resource as fallback. Shared units in `source/shared/` (ICU.pas, BCP47.pas, IntlTypes.pas, IntlICU.pas, IntlCLDRData.pas, IntlLocaleResolver.pas) provide platform-independent Intl infrastructure. Existing locale stubs on String/Number/Date prototypes wired to Intl formatters. [built-ins-intl.md](built-ins-intl.md). [build-system.md § Generated Intl Data](build-system.md#generated-intl-data). diff --git a/docs/embedding.md b/docs/embedding.md index 26609885..4da0c93d 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -520,6 +520,7 @@ try cfNonStrictMode, cfLabel, cfTraditionalFor, + cfForIn, cfWhileLoops ]; // Enable selected compatibility semantics Engine.SourceType := stModule; // Run entry as module source (top-level this is undefined; import.meta resolves) diff --git a/docs/errors.md b/docs/errors.md index 526d0001..2bfc93fa 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -82,6 +82,7 @@ Other features that produce suggestions include: - `function` declarations and expressions -- suggests arrow functions - `==` / `!=` (loose equality) -- suggests `===` / `!==` - Traditional `for(init; test; update)` loops when `--compat-traditional-for-loop` is off -- suggests `for...of`, array methods, or the flag +- `for...in` loops when `--compat-for-in-loop` is off -- suggests `Object.keys()` / `Object.entries()` with `for...of`, or the flag - `while` / `do...while` loops when `--compat-while-loops` is off -- suggests `for...of`, array methods, or the flag See [Language](language.md) for the full list of excluded features and their rationale. diff --git a/docs/goals.md b/docs/goals.md index 0910ee74..635e65a8 100644 --- a/docs/goals.md +++ b/docs/goals.md @@ -36,7 +36,7 @@ Desktop applications built with FreePascal (Lazarus, command-line tools, game en ## What GocciaScript is Not - **Not a Node.js replacement** — No `require()`, no `node:` built-in modules, no event loop with I/O callbacks -- **Not aiming for 100% ECMAScript conformance** — Features excluded by design, such as `eval`, will not be added. `var`, the `function` keyword, `arguments`/`with`/`delete`/`this` non-strict semantics, loose equality, labels, the traditional `for(init; test; update)` loop, and `while`/`do...while` loops are disabled by default and exposed through targeted JavaScript compatibility toggles (`--compat-var`, `--compat-function`, `--compat-non-strict-mode`, `--compat-loose-equality`, `--compat-label`, `--compat-traditional-for-loop`, `--compat-while-loops`) for cases such as conformance tests or legacy semantic requirements. Runtime enforcement of type annotations is also opt-in (`--strict-types`). See [Language](language.md) for the full list. +- **Not aiming for 100% ECMAScript conformance** — Features excluded by design, such as `eval`, will not be added. `var`, the `function` keyword, `arguments`/`with`/`delete`/`this` non-strict semantics, loose equality, labels, the traditional `for(init; test; update)` loop, `for...in`, and `while`/`do...while` loops are disabled by default and exposed through targeted JavaScript compatibility toggles (`--compat-var`, `--compat-function`, `--compat-non-strict-mode`, `--compat-loose-equality`, `--compat-label`, `--compat-traditional-for-loop`, `--compat-for-in-loop`, `--compat-while-loops`) for cases such as conformance tests or legacy semantic requirements. Runtime enforcement of type annotations is also opt-in (`--strict-types`). See [Language](language.md) for the full list. - **Not a formally verified sandbox** — The sandbox reduces attack surface but has not been independently audited - **Not performance-competitive with V8/SpiderMonkey** — GocciaScript prioritizes correctness, embeddability, and reduced attack surface over raw throughput diff --git a/docs/language-tables.md b/docs/language-tables.md index b6dbc9fc..ab2df98c 100644 --- a/docs/language-tables.md +++ b/docs/language-tables.md @@ -23,6 +23,7 @@ | Non-strict assignment failures | ES1 | Strict by default; script source `--compat-non-strict-mode` silently ignores failed ordinary object/global writes while assignment expressions return the assigned value | | Labeled statements | ES1 | Opt-in for JavaScript compatibility (`--compat-label`); disabled by default | | Traditional `for(init; test; update)` loop | ES1 | Opt-in for JavaScript compatibility (`--compat-traditional-for-loop`); disabled by default | +| `for...in` | ES1 | Opt-in for JavaScript compatibility (`--compat-for-in-loop`); disabled by default | | `while` / `do...while` | ES1 | Opt-in for JavaScript compatibility (`--compat-while-loops`); disabled by default | | `with` statement | ES1 | Opt-in for script source (`--compat-non-strict-mode`) for compatibility with object-environment lookup, `Symbol.unscopables`, closure capture, method-call receivers, and non-strict write failures — prefer explicit property access | | `delete` non-strict return values | ES1 | Strict by default; script source `--compat-non-strict-mode` makes `delete identifier` handle declared bindings, configurable global object properties, and unresolvable names with legacy booleans; non-configurable property deletion returns `false` | diff --git a/docs/language.md b/docs/language.md index e543acfe..87dcbe5e 100644 --- a/docs/language.md +++ b/docs/language.md @@ -8,8 +8,8 @@ - **Modern subset** — `let`/`const`, arrow functions, classes with private fields, `for...of`, async/await, ES modules - **TC39 proposals** — Decorators, decorator metadata, pattern matching, types as comments, enums, `Math.clamp` - **Excluded by design** — `eval`, wildcard re-exports -- **Graceful handling** — Parser-recognized excluded or disabled syntax (`==`/`!=` when `--compat-loose-equality` is off, labels when `--compat-label` is off, `while`/`do...while` when `--compat-while-loops` is off, `with` when `--compat-non-strict-mode` is off, traditional `for(;;)` when `--compat-traditional-for-loop` is off) parses successfully but executes as a no-op with a warning and suggestion -- **Opt-in toggles** — ASI (`--compat-asi`), `var` declarations (`--compat-var`), `function` keyword (`--compat-function`), non-strict Script compatibility (`--compat-non-strict-mode` for `arguments`, `with`, silent assignment failures, legacy `delete` returns, and sloppy `this`), loose equality (`--compat-loose-equality`), labels (`--compat-label`), traditional `for(init; test; update)` loops (`--compat-traditional-for-loop`), `while`/`do...while` loops (`--compat-while-loops`), runtime type enforcement (`--strict-types`) +- **Graceful handling** — Parser-recognized excluded or disabled syntax (`==`/`!=` when `--compat-loose-equality` is off, labels when `--compat-label` is off, `while`/`do...while` when `--compat-while-loops` is off, `with` when `--compat-non-strict-mode` is off, traditional `for(;;)` when `--compat-traditional-for-loop` is off, `for...in` when `--compat-for-in-loop` is off) parses successfully but executes as a no-op with a warning and suggestion +- **Opt-in toggles** — ASI (`--compat-asi`), `var` declarations (`--compat-var`), `function` keyword (`--compat-function`), non-strict Script compatibility (`--compat-non-strict-mode` for `arguments`, `with`, silent assignment failures, legacy `delete` returns, and sloppy `this`), loose equality (`--compat-loose-equality`), labels (`--compat-label`), traditional `for(init; test; update)` loops (`--compat-traditional-for-loop`), `for...in` loops (`--compat-for-in-loop`), `while`/`do...while` loops (`--compat-while-loops`), runtime type enforcement (`--strict-types`) - **Default preprocessors** — JSX (enabled by default via `DefaultPreprocessors`) GocciaScript implements a curated subset of ECMAScript. This document details what's supported, what's excluded, and the rationale for each decision. For quick-reference tables of every feature and TC39 proposal, see [Language Tables](language-tables.md). @@ -133,6 +133,7 @@ class Counter { - `return` - Block statements - `for...of` and `for await...of` (see [Supported Iteration](#supported-iteration)) +- `for...in` via `--compat-for-in-loop` (see [`for...in`](#forin-loop)) - `while` and `do...while` via `--compat-while-loops` (see [`while` and `do...while`](#while-and-dowhile)) - `import`/`export` (ES module system) @@ -665,7 +666,7 @@ console.log(x); // 5 With `let`/`const`, accessing before declaration is a `ReferenceError` (Temporal Dead Zone), which catches bugs early. -When enabled (CLI: `--compat-var`, engine API: include `cfVar` in `Engine.Compatibility`, config: `{"compat-var": true}`), `var` declarations follow ES2026 §14.3.2 semantics: function-scoped (escapes blocks), hoisted to function top as `undefined`, redeclaration allowed, no TDZ, with destructuring and for-of support. Var bindings are stored in a separate binding map (`FVarBindings`) on function/module/global scopes, distinct from lexical bindings. See [interpreter.md § Scope Chain Design](interpreter.md#scope-chain-design). +When enabled (CLI: `--compat-var`, engine API: include `cfVar` in `Engine.Compatibility`, config: `{"compat-var": true}`), `var` declarations follow ES2026 §14.3.2 semantics: function-scoped (escapes blocks), hoisted to function top as `undefined`, redeclaration allowed, no TDZ, with destructuring, for-of, and enabled for-in support. Var bindings are stored in a separate binding map (`FVarBindings`) on function/module/global scopes, distinct from lexical bindings. See [interpreter.md § Scope Chain Design](interpreter.md#scope-chain-design). ### `function` Keyword @@ -796,7 +797,27 @@ items.filter((item) => item.isValid); items.reduce((acc, item) => acc + item, 0); ``` -`break` exits the nearest enclosing `switch`, `for...of`, `for await...of`, enabled traditional `for` loop, or enabled `while`/`do...while` loop. `continue` only applies to iteration constructs — `for...of`, `for await...of`, enabled traditional `for`, and enabled `while`/`do...while` — never to `switch`. +`break` exits the nearest enclosing `switch`, `for...of`, `for await...of`, enabled traditional `for` loop, enabled `for...in` loop, or enabled `while`/`do...while` loop. `continue` only applies to iteration constructs — `for...of`, `for await...of`, enabled traditional `for`, enabled `for...in`, and enabled `while`/`do...while` — never to `switch`. + +### `for...in` Loop + +**Opt-in for JavaScript compatibility.** Excluded by default. Available via `--compat-for-in-loop` (CLI flag, `cfForIn` in `Engine.Compatibility`, or `{"compat-for-in-loop": true}` in config) when a program or conformance suite needs ECMAScript property enumeration semantics. + +When disabled (default), the parser accepts `for...in` declaration and assignment-target forms but treats the loop as a no-op and emits a warning. When enabled, declaration-head loops such as `for (const key in object) body` / `for (let key in object) body` and assignment-target loops such as `for (key in object) body` are supported in interpreter and bytecode modes: + +- The right-hand side follows ECMAScript `ForIn/OfHeadEvaluation`: `null` and `undefined` produce an empty loop; other primitives are boxed with `ToObject`. +- The loop enumerates enumerable string property names, including inherited enumerable properties, and never returns symbol keys. +- A property name appears at most once. An own property shadows a prototype property with the same name even when the own property is non-enumerable. +- `var` declaration heads, including destructuring heads, require both `--compat-var` and `--compat-for-in-loop`; the bindings hoist out of the loop and are shared across iterations. +- `break`, `continue`, and `return` unwind as they do in other supported loops. + +New GocciaScript code should usually prefer `Object.keys(obj)`, `Object.entries(obj)`, or explicit `for...of` over property names: + +```javascript +for (const key of Object.keys(obj)) { + // ... +} +``` ### `while` and `do...while` @@ -850,8 +871,8 @@ The labeled statement itself (the statement after the `:`) is still parsed and e When enabled, labels can target `break` and `continue` statements in interpreter and bytecode modes: -- `break label;` exits the matching enclosing labeled statement, including labeled blocks, `switch`, `for...of`, `for await...of`, traditional `for(;;)` with `--compat-traditional-for-loop`, and `while`/`do...while` with `--compat-while-loops`. -- `continue label;` targets matching enclosing labeled iteration statements only: `for...of`, `for await...of`, traditional `for(;;)` with `--compat-traditional-for-loop`, and `while`/`do...while` with `--compat-while-loops`. +- `break label;` exits the matching enclosing labeled statement, including labeled blocks, `switch`, `for...of`, `for await...of`, traditional `for(;;)` with `--compat-traditional-for-loop`, `for...in` with `--compat-for-in-loop`, and `while`/`do...while` with `--compat-while-loops`. +- `continue label;` targets matching enclosing iteration statements only: `for...of`, `for await...of`, traditional `for(;;)` with `--compat-traditional-for-loop`, `for...in` with `--compat-for-in-loop`, and `while`/`do...while` with `--compat-while-loops`. ### Generators and Iterators diff --git a/docs/test262.md b/docs/test262.md index 67f941d0..93e9d3a0 100644 --- a/docs/test262.md +++ b/docs/test262.md @@ -256,10 +256,15 @@ Script tests also receive the flag, but the injected directive keeps `arguments`, `with`, non-strict assignment failures, legacy `delete` return values, and regular-function nullish `this` coercion on the strict path. Remaining `noStrict` tests rely on sloppy-only behaviors that -GocciaScript still does not provide and fail naturally. They are -documented in `scripts/test262_compatibility_roadmap.json` as -`excluded-by-language-design` and counted as expected failures, not as -wrapper-infra failures. +GocciaScript still does not provide and fail naturally as ordinary +conformance failures, not as wrapper-infra failures. + +The runner passes syntax compatibility flags such as +`--compat-traditional-for-loop`, `--compat-for-in-loop`, +`--compat-while-loops`, and `--compat-label` unconditionally because +test262 uses those forms across both harness helpers and test bodies; the +test's source type and strictness still decide runtime strict-mode +semantics. ## Path normalization diff --git a/docs/tutorial.md b/docs/tutorial.md index 1f7f98dd..ea1f420f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -313,6 +313,7 @@ Here's a quick reference of GocciaScript's key restrictions: | `==` / `!=` | Off by default | `===` / `!==` or `--compat-loose-equality` | | labels / `break label` / `continue label` | Off by default | `return` from a helper, an early-exit flag, or `--compat-label` for JavaScript compatibility | | `for (init; test; update)` | Off by default | `for...of`, `.map()`, `.forEach()`, `.reduce()`, or `--compat-traditional-for-loop` for JavaScript compatibility | +| `for (key in object)` | Off by default | `Object.keys()` / `Object.entries()` with `for...of`, or `--compat-for-in-loop` for JavaScript compatibility | | `while (...)` / `do ... while (...)` | Off by default | `for...of`, `.map()`, `.forEach()`, `.reduce()`, or `--compat-while-loops` for JavaScript compatibility | | `eval("code")` | Not supported | No alternative (by design) | | `arguments` | Off by default | Prefer rest parameters (`...args`) or script source `--compat-non-strict-mode` | diff --git a/scripts/run_test262_suite.ts b/scripts/run_test262_suite.ts index b20936d3..61bf8f03 100644 --- a/scripts/run_test262_suite.ts +++ b/scripts/run_test262_suite.ts @@ -665,17 +665,20 @@ interface PerTestRecord { // test262 source is overwhelmingly semicolon-omitted; ASI is required to parse // the corpus. --compat-var, --compat-function, --compat-traditional-for-loop, -// --compat-while-loops, --compat-loose-equality, --compat-label, and --unsafe-function-constructor are also -// unconditional: stock harness uses `var`, `function`, traditional `for(;;)` -// loops, while/do-while loops, loose equality, labels, and `Function("return this;")()`. Non-strict mode -// compatibility is also enabled for script source strict tests: strict -// directives and modules decide strict semantics, while the flag exposes -// compatibility-gated syntax and implicit objects needed by the corpus. +// --compat-for-in-loop, --compat-while-loops, --compat-loose-equality, +// --compat-label, and --unsafe-function-constructor are also unconditional: +// stock harness and tests use `var`, `function`, traditional `for(;;)` loops, +// for-in loops, while/do-while loops, loose equality, labels, and +// `Function("return this;")()`. Non-strict mode compatibility is enabled +// for Script tests separately: strict directives and modules decide strict +// semantics, while the flag exposes compatibility-gated syntax and implicit +// objects needed by the corpus. const TEST262_BARE_FLAGS: readonly string[] = [ "--compat-asi", "--compat-var", "--compat-function", "--compat-traditional-for-loop", + "--compat-for-in-loop", "--compat-while-loops", "--compat-loose-equality", "--compat-label", diff --git a/scripts/test-cli.ts b/scripts/test-cli.ts index 5000018d..caeac3c4 100644 --- a/scripts/test-cli.ts +++ b/scripts/test-cli.ts @@ -4,7 +4,7 @@ * * Common CLI options tested across all apps: stdin smoke, --help, --unsafe-ffi, * --compat-asi, --source-type, .mjs source-type inference, --compat-var, --compat-loose-equality, --compat-non-strict-mode, - * --compat-while-loops, --mode, --timeout, --max-instructions, --max-memory, --stack-size, --log, + * --compat-for-in-loop, --compat-while-loops, --mode, --timeout, --max-instructions, --max-memory, --stack-size, --log, * example scripts. */ @@ -226,6 +226,11 @@ console.log("--compat-function (Loader) + Bare loader compat parsing..."); const forOut = await $`${BARE} --print ${forSrc} --compat-traditional-for-loop 2>&1`.text(); if (forOut.trim() !== "15") throw new Error(`Bare --compat-traditional-for-loop expected 15, got: ${forOut}`); + const forInSrc = join(tmp, "use-for-in.js"); + writeFileSync(forInSrc, "const obj = { a: 1, b: 2 };\nlet out = '';\nfor (const k in obj) { out = out + k; }\nout;\n"); + const forInOut = await $`${BARE} --print ${forInSrc} --compat-for-in-loop 2>&1`.text(); + if (forInOut.trim() !== "ab") throw new Error(`Bare --compat-for-in-loop expected ab, got: ${forInOut}`); + const whileSrc = join(tmp, "use-while.js"); writeFileSync(whileSrc, "let s = 0;\nlet i = 1;\nwhile (i <= 5) { s = s + i; i++; }\ns;\n"); const whileOut = await $`${BARE} --print ${whileSrc} --compat-while-loops 2>&1`.text(); diff --git a/scripts/test262_compatibility_roadmap.json b/scripts/test262_compatibility_roadmap.json deleted file mode 100644 index d62739df..00000000 --- a/scripts/test262_compatibility_roadmap.json +++ /dev/null @@ -1,1349 +0,0 @@ -{ - "eligibleStatuses": [ - "supported", - "active", - "compat" - ], - "features": { - "AggregateError": { - "status": "supported", - "target": "globals-errors" - }, - "Array.from": { - "status": "supported", - "target": "arrays" - }, - "Array.fromAsync": { - "status": "supported", - "target": "arrays" - }, - "Array.of": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.at": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.entries": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.find": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.findIndex": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.findLast": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.findLastIndex": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.flat": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.flatMap": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.includes": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.keys": { - "status": "supported", - "target": "arrays" - }, - "Array.prototype.values": { - "status": "supported", - "target": "arrays" - }, - "ArrayBuffer": { - "status": "supported", - "target": "arrays" - }, - "Atomics": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "Atomics.pause": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "Atomics.waitAsync": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "BigInt": { - "status": "supported", - "target": "numbers-bigint" - }, - "BigInt64Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "BigUint64Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "DataView": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getFloat32": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getFloat64": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getInt16": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getInt32": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getInt8": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getUint16": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.getUint32": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "DataView.prototype.setUint8": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "Error.isError": { - "status": "supported", - "target": "globals-errors" - }, - "FinalizationRegistry": { - "status": "unsupported", - "target": "weakrefs" - }, - "Float16Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Float32Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Float64Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Int16Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Int32Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Int8Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Intl": { - "status": "implemented", - "target": "intl" - }, - "Intl-enumeration": { - "status": "implemented", - "target": "intl" - }, - "Intl.DateTimeFormat": { - "status": "implemented", - "target": "intl" - }, - "Intl.DisplayNames": { - "status": "implemented", - "target": "intl" - }, - "Intl.DurationFormat": { - "status": "implemented", - "target": "intl" - }, - "Intl.Era-monthcode": { - "status": "in-progress", - "target": "intl" - }, - "Intl.ListFormat": { - "status": "implemented", - "target": "intl" - }, - "Intl.Locale": { - "status": "implemented", - "target": "intl" - }, - "Intl.NumberFormat": { - "status": "implemented", - "target": "intl" - }, - "Intl.RelativeTimeFormat": { - "status": "implemented", - "target": "intl" - }, - "Intl.Segmenter": { - "status": "implemented", - "target": "intl" - }, - "IsHTMLDDA": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "JSON": { - "status": "supported", - "target": "json" - }, - "Map": { - "status": "supported", - "target": "collections" - }, - "Map.groupBy": { - "status": "supported", - "target": "collections" - }, - "Math.acosh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.asinh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.atanh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.cbrt": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.clz32": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.cosh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.expm1": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.fround": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.hypot": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.imul": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.log10": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.log1p": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.log2": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.sign": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.sinh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.sumPrecise": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.tanh": { - "status": "supported", - "target": "numbers-bigint" - }, - "Math.trunc": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.isFinite": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.isInteger": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.isNaN": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.isSafeInteger": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.parseFloat": { - "status": "supported", - "target": "numbers-bigint" - }, - "Number.parseInt": { - "status": "supported", - "target": "numbers-bigint" - }, - "Object.assign": { - "status": "supported", - "target": "unassigned" - }, - "Object.entries": { - "status": "supported", - "target": "unassigned" - }, - "Object.fromEntries": { - "status": "supported", - "target": "unassigned" - }, - "Object.groupBy": { - "status": "supported", - "target": "collections" - }, - "Object.hasOwn": { - "status": "supported", - "target": "unassigned" - }, - "Object.is": { - "status": "supported", - "target": "unassigned" - }, - "Object.keys": { - "status": "supported", - "target": "unassigned" - }, - "Object.values": { - "status": "supported", - "target": "unassigned" - }, - "Promise": { - "status": "supported", - "target": "async-promises" - }, - "Promise.allSettled": { - "status": "supported", - "target": "async-promises" - }, - "Promise.any": { - "status": "supported", - "target": "async-promises" - }, - "Promise.prototype.finally": { - "status": "supported", - "target": "async-promises" - }, - "Proxy": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.apply": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.construct": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.defineProperty": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.deleteProperty": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.get": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.getOwnPropertyDescriptor": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.getPrototypeOf": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.has": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.isExtensible": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.ownKeys": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.preventExtensions": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.set": { - "status": "supported", - "target": "proxy-reflect" - }, - "Reflect.setPrototypeOf": { - "status": "supported", - "target": "proxy-reflect" - }, - "RegExp": { - "status": "supported", - "target": "regexp" - }, - "RegExp.escape": { - "status": "supported", - "target": "regexp" - }, - "Set": { - "status": "supported", - "target": "collections" - }, - "ShadowRealm": { - "status": "unsupported", - "target": "realms" - }, - "SharedArrayBuffer": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "String.fromCodePoint": { - "status": "supported", - "target": "strings" - }, - "String.prototype.at": { - "status": "supported", - "target": "strings" - }, - "String.prototype.endsWith": { - "status": "supported", - "target": "strings" - }, - "String.prototype.includes": { - "status": "supported", - "target": "strings" - }, - "String.prototype.isWellFormed": { - "status": "supported", - "target": "strings" - }, - "String.prototype.matchAll": { - "status": "supported", - "target": "strings" - }, - "String.prototype.padEnd": { - "status": "supported", - "target": "strings" - }, - "String.prototype.padStart": { - "status": "supported", - "target": "strings" - }, - "String.prototype.repeat": { - "status": "supported", - "target": "strings" - }, - "String.prototype.replaceAll": { - "status": "supported", - "target": "strings" - }, - "String.prototype.startsWith": { - "status": "supported", - "target": "strings" - }, - "String.prototype.toWellFormed": { - "status": "supported", - "target": "strings" - }, - "String.prototype.trim": { - "status": "supported", - "target": "strings" - }, - "String.prototype.trimEnd": { - "status": "supported", - "target": "strings" - }, - "String.prototype.trimStart": { - "status": "supported", - "target": "strings" - }, - "String.raw": { - "status": "supported", - "target": "strings" - }, - "Symbol": { - "status": "supported", - "target": "symbols" - }, - "Symbol.asyncIterator": { - "status": "supported", - "target": "symbols" - }, - "Symbol.for": { - "status": "supported", - "target": "symbols" - }, - "Symbol.hasInstance": { - "status": "supported", - "target": "symbols" - }, - "Symbol.isConcatSpreadable": { - "status": "supported", - "target": "symbols" - }, - "Symbol.iterator": { - "status": "supported", - "target": "symbols" - }, - "Symbol.match": { - "status": "supported", - "target": "symbols" - }, - "Symbol.matchAll": { - "status": "supported", - "target": "regexp" - }, - "Symbol.prototype.description": { - "status": "supported", - "target": "symbols" - }, - "Symbol.replace": { - "status": "supported", - "target": "symbols" - }, - "Symbol.search": { - "status": "supported", - "target": "symbols" - }, - "Symbol.species": { - "status": "supported", - "target": "symbols" - }, - "Symbol.split": { - "status": "supported", - "target": "symbols" - }, - "Symbol.toPrimitive": { - "status": "supported", - "target": "symbols" - }, - "Symbol.toStringTag": { - "status": "supported", - "target": "symbols" - }, - "Symbol.unscopables": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "Temporal": { - "status": "supported", - "target": "temporal" - }, - "TypedArray": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "TypedArray.prototype.at": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Uint16Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Uint32Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Uint8Array": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "Uint8ClampedArray": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "WeakMap": { - "status": "supported", - "target": "collections" - }, - "WeakRef": { - "status": "unsupported", - "target": "weakrefs" - }, - "WeakSet": { - "status": "supported", - "target": "collections" - }, - "__getter__": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "__proto__": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "__setter__": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "align-detached-buffer-semantics-with-web-reality": { - "status": "active", - "target": "arraybuffer-typedarray" - }, - "arbitrary-module-namespace-names": { - "status": "unsupported", - "target": "modules" - }, - "array-find-from-last": { - "status": "supported", - "target": "arrays" - }, - "array-grouping": { - "status": "supported", - "target": "arrays" - }, - "arraybuffer-transfer": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "arrow-function": { - "status": "supported", - "target": "syntax" - }, - "async-await": { - "status": "supported", - "target": "async-promises" - }, - "async-functions": { - "status": "supported", - "target": "async-promises" - }, - "async-generator": { - "status": "supported", - "target": "iterators" - }, - "async-generators": { - "status": "supported", - "target": "iterators" - }, - "async-iteration": { - "status": "supported", - "target": "async-promises" - }, - "await-dictionary": { - "status": "unsupported", - "target": "proposals" - }, - "caller": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "change-array-by-copy": { - "status": "supported", - "target": "arrays" - }, - "class": { - "status": "supported", - "target": "classes" - }, - "class-fields-private": { - "status": "supported", - "target": "classes" - }, - "class-fields-private-in": { - "status": "supported", - "target": "classes" - }, - "class-fields-public": { - "status": "supported", - "target": "classes" - }, - "class-methods-private": { - "status": "supported", - "target": "classes" - }, - "class-static-block": { - "status": "supported", - "target": "classes" - }, - "class-static-fields-private": { - "status": "supported", - "target": "classes" - }, - "class-static-fields-public": { - "status": "supported", - "target": "classes" - }, - "class-static-methods-private": { - "status": "supported", - "target": "classes" - }, - "coalesce-expression": { - "status": "supported", - "target": "unassigned" - }, - "computed-property-names": { - "status": "supported", - "target": "unassigned" - }, - "const": { - "status": "supported", - "target": "syntax" - }, - "cross-realm": { - "status": "unsupported", - "target": "realms" - }, - "decorators": { - "status": "supported", - "target": "classes" - }, - "default-parameters": { - "status": "supported", - "target": "syntax" - }, - "destructuring-assignment": { - "status": "supported", - "target": "bindings-patterns" - }, - "destructuring-binding": { - "status": "supported", - "target": "bindings-patterns" - }, - "disposition": { - "status": "supported", - "target": "resource-management" - }, - "dynamic-import": { - "status": "supported", - "target": "modules" - }, - "error-cause": { - "status": "supported", - "target": "globals-errors" - }, - "explicit-resource-management": { - "status": "supported", - "target": "resource-management" - }, - "exponentiation": { - "status": "supported", - "target": "unassigned" - }, - "export-star-as-namespace-from-module": { - "status": "unsupported", - "target": "modules" - }, - "for-in-order": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "for-of": { - "status": "supported", - "target": "unassigned" - }, - "generators": { - "status": "supported", - "target": "iterators" - }, - "globalThis": { - "status": "supported", - "target": "globals-errors" - }, - "hashbang": { - "status": "supported", - "target": "syntax" - }, - "immutable-arraybuffer": { - "status": "active", - "target": "arraybuffer-typedarray" - }, - "import-assertions": { - "status": "unsupported", - "target": "modules" - }, - "import-attributes": { - "status": "unsupported", - "target": "modules" - }, - "import-bytes": { - "status": "unsupported", - "target": "modules" - }, - "import-defer": { - "status": "unsupported", - "target": "modules" - }, - "import-text": { - "status": "unsupported", - "target": "modules" - }, - "import.meta": { - "status": "supported", - "target": "modules" - }, - "iterator-helpers": { - "status": "supported", - "target": "iterators" - }, - "iterator-sequencing": { - "status": "active", - "target": "iterators" - }, - "joint-iteration": { - "status": "active", - "target": "iterators" - }, - "json-modules": { - "status": "unsupported", - "target": "modules" - }, - "json-parse-with-source": { - "status": "unsupported", - "target": "json" - }, - "json-superset": { - "status": "supported", - "target": "syntax" - }, - "legacy-regexp": { - "status": "excluded-by-language-design", - "target": "legacy-web-compat" - }, - "let": { - "status": "supported", - "target": "syntax" - }, - "logical-assignment-operators": { - "status": "supported", - "target": "unassigned" - }, - "new.target": { - "status": "supported", - "target": "syntax" - }, - "nonextensible-applies-to-private": { - "status": "active", - "target": "classes" - }, - "nullish-coalescing": { - "status": "supported", - "target": "unassigned" - }, - "numeric-separator-literal": { - "status": "supported", - "target": "syntax" - }, - "object-rest": { - "status": "supported", - "target": "bindings-patterns" - }, - "object-spread": { - "status": "supported", - "target": "bindings-patterns" - }, - "optional-catch-binding": { - "status": "supported", - "target": "unassigned" - }, - "optional-chaining": { - "status": "supported", - "target": "unassigned" - }, - "promise-try": { - "status": "supported", - "target": "async-promises" - }, - "promise-with-resolvers": { - "status": "supported", - "target": "async-promises" - }, - "proxy-missing-checks": { - "status": "active", - "target": "proxy-reflect" - }, - "regexp-dotall": { - "status": "supported", - "target": "regexp" - }, - "regexp-duplicate-named-groups": { - "status": "supported", - "target": "regexp" - }, - "regexp-lookbehind": { - "status": "supported", - "target": "regexp" - }, - "regexp-match-indices": { - "status": "supported", - "target": "regexp" - }, - "regexp-modifiers": { - "status": "supported", - "target": "regexp" - }, - "regexp-named-groups": { - "status": "supported", - "target": "regexp" - }, - "regexp-unicode-property-escapes": { - "status": "supported", - "target": "regexp" - }, - "regexp-v-flag": { - "status": "supported", - "target": "regexp" - }, - "resizable-arraybuffer": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "rest-parameters": { - "status": "supported", - "target": "syntax" - }, - "set-methods": { - "status": "supported", - "target": "collections" - }, - "source-phase-imports": { - "status": "unsupported", - "target": "modules" - }, - "source-phase-imports-module-source": { - "status": "unsupported", - "target": "modules" - }, - "spread": { - "status": "supported", - "target": "syntax" - }, - "stable-array-sort": { - "status": "supported", - "target": "arrays" - }, - "stable-typedarray-sort": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "string-trimming": { - "status": "supported", - "target": "strings" - }, - "structuredClone": { - "status": "supported", - "target": "globals-errors" - }, - "super": { - "status": "supported", - "target": "syntax" - }, - "symbols-as-weakmap-keys": { - "status": "supported", - "target": "collections" - }, - "tail-call-optimization": { - "status": "unsupported", - "target": "functions" - }, - "template": { - "status": "supported", - "target": "syntax" - }, - "template-literal": { - "status": "supported", - "target": "syntax" - }, - "top-level-await": { - "status": "supported", - "target": "async-promises" - }, - "u180e": { - "status": "supported", - "target": "strings" - }, - "uint8array-base64": { - "status": "supported", - "target": "arraybuffer-typedarray" - }, - "upsert": { - "status": "supported", - "target": "collections" - }, - "well-formed-json-stringify": { - "status": "supported", - "target": "json" - }, - "well-known-symbol": { - "status": "supported", - "target": "symbols" - } - }, - "flags": { - "CanBlockIsFalse": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "CanBlockIsTrue": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "async": { - "status": "supported", - "target": "harness" - }, - "generated": { - "status": "supported", - "target": "harness" - }, - "module": { - "status": "unsupported", - "target": "modules" - }, - "noStrict": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "non-deterministic": { - "status": "unsupported", - "target": "harness" - }, - "onlyStrict": { - "status": "supported", - "target": "syntax" - }, - "raw": { - "status": "unsupported", - "target": "syntax" - } - }, - "harnessIncludes": { - "assertRelativeDateMs.js": { - "status": "supported", - "target": "harness" - }, - "asyncHelpers.js": { - "status": "unsupported", - "target": "async-promises" - }, - "atomicsHelper.js": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "iteratorZipUtils.js": { - "status": "unsupported", - "target": "iterators" - }, - "resizableArrayBufferUtils.js": { - "status": "unsupported", - "target": "arraybuffer-typedarray" - }, - "sm/assertThrowsValue.js": { - "status": "unsupported", - "target": "harness" - }, - "sm/non262-Date-shell.js": { - "status": "unsupported", - "target": "globals-errors" - }, - "sm/non262-JSON-shell.js": { - "status": "unsupported", - "target": "json" - }, - "sm/non262-Math-shell.js": { - "status": "unsupported", - "target": "numbers-bigint" - }, - "sm/non262-Reflect-shell.js": { - "status": "unsupported", - "target": "proxy-reflect" - }, - "sm/non262-Set-shell.js": { - "status": "unsupported", - "target": "collections" - }, - "sm/non262-TypedArray-shell.js": { - "status": "unsupported", - "target": "arraybuffer-typedarray" - }, - "sm/non262-expressions-shell.js": { - "status": "unsupported", - "target": "harness" - }, - "sm/non262-generators-shell.js": { - "status": "unsupported", - "target": "iterators" - }, - "sm/non262-strict-shell.js": { - "status": "unsupported", - "target": "harness" - }, - "tcoHelper.js": { - "status": "unsupported", - "target": "harness" - }, - "testAtomics.js": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "testIntl.js": { - "status": "supported", - "target": "intl" - }, - "wellKnownIntrinsicObjects.js": { - "status": "supported", - "target": "harness" - } - }, - "pathSegments": { - "Atomics": { - "status": "unsupported", - "target": "atomics-shared-memory" - }, - "DataView": { - "status": "unsupported", - "target": "binary-data-dataview" - }, - "WeakRef": { - "status": "unsupported", - "target": "weakrefs" - }, - "eval-code": { - "status": "unsupported", - "target": "excluded-syntax" - }, - "for-in": { - "status": "unsupported", - "target": "excluded-syntax" - } - }, - "syntax": { - "arguments_object": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "do_while_loop": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "eval_identifier": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "for_in_loop": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "function_declaration": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "function_expression": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "labeled_break": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "labeled_continue": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "labeled_statement": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "loose_equality": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "loose_inequality": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "new_function": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "traditional_for_loop": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "while_loop": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - }, - "with_statement": { - "status": "excluded-by-language-design", - "target": "excluded-syntax" - } - }, - "targets": { - "arraybuffer-typedarray": { - "description": "ArrayBuffer, SharedArrayBuffer, typed arrays, and Uint8Array encodings excluding DataView", - "status": "active" - }, - "arrays": { - "description": "Array constructor and prototype behavior", - "status": "active" - }, - "async-promises": { - "description": "Promises, async functions, async iteration, and top-level await", - "status": "active" - }, - "atomics-shared-memory": { - "description": "Atomics and blocking shared-memory primitives are not implemented", - "status": "unsupported" - }, - "binary-data-dataview": { - "description": "DataView is not implemented", - "status": "unsupported" - }, - "bindings-patterns": { - "description": "Binding, destructuring, var hoisting, and iterator-close semantics", - "status": "active" - }, - "classes": { - "description": "Class fields, private names, decorators, and related semantics", - "status": "active" - }, - "collections": { - "description": "Map, Set, weak collections, grouping, and set methods", - "status": "active" - }, - "excluded-syntax": { - "description": "Syntax excluded by GocciaScript language design", - "status": "excluded-by-language-design" - }, - "globals-errors": { - "description": "Global APIs and Error family behavior", - "status": "active" - }, - "functions": { - "description": "Function object, call-frame, and tail-call semantics", - "status": "active" - }, - "harness": { - "description": "test262 harness helper coverage", - "status": "active" - }, - "intl": { - "description": "ECMA-402 Internationalization API via system ICU with embedded CLDR fallback", - "status": "implemented" - }, - "iterators": { - "description": "Generators and iterator helpers", - "status": "active" - }, - "json": { - "description": "JSON parse/stringify and proposal extensions", - "status": "active" - }, - "legacy-web-compat": { - "description": "Legacy web compatibility hooks are intentionally excluded", - "status": "excluded-by-language-design" - }, - "modules": { - "description": "Module compatibility beyond GocciaScript named imports/exports", - "status": "unsupported" - }, - "numbers-bigint": { - "description": "Number, Math, BigInt, and numeric operator behavior", - "status": "active" - }, - "proposals": { - "description": "Unimplemented proposal or staging-area features", - "status": "unsupported" - }, - "proxy-reflect": { - "description": "Proxy and Reflect invariants", - "status": "active" - }, - "realms": { - "description": "Cross-realm and ShadowRealm semantics are not implemented", - "status": "unsupported" - }, - "regexp": { - "description": "RegExp syntax and built-ins", - "status": "active" - }, - "resource-management": { - "description": "Explicit resource management built-ins and syntax", - "status": "active" - }, - "roadmap-hygiene": { - "description": "Tests requiring explicit classification before they should run", - "status": "active" - }, - "strings": { - "description": "String constructor and prototype behavior", - "status": "active" - }, - "symbols": { - "description": "Symbol constructor, well-known symbols, and symbol wrappers", - "status": "active" - }, - "syntax": { - "description": "Core supported syntax and parser compatibility", - "status": "active" - }, - "temporal": { - "description": "Temporal built-ins", - "status": "active" - }, - "unassigned": { - "description": "Supported tags not yet assigned to a narrower target", - "status": "active" - }, - "weakrefs": { - "description": "WeakRef and FinalizationRegistry are not implemented", - "status": "unsupported" - } - }, - "unknownFeature": { - "eligible": false, - "status": "needs-roadmap-entry", - "target": "roadmap-hygiene" - }, - "version": 1 -} diff --git a/source/app/Goccia.CLI.Options.pas b/source/app/Goccia.CLI.Options.pas index 765d6dd5..19c52acd 100644 --- a/source/app/Goccia.CLI.Options.pas +++ b/source/app/Goccia.CLI.Options.pas @@ -124,7 +124,9 @@ implementation (OptionName: 'compat-non-strict-mode'; HelpText: 'Enable non-strict-mode compatibility semantics'), (OptionName: 'compat-label'; - HelpText: 'Enable labeled break and continue targets (compatibility)') + HelpText: 'Enable labeled break and continue targets (compatibility)'), + (OptionName: 'compat-for-in-loop'; + HelpText: 'Enable for...in property enumeration loops (compatibility)') ); function CompatibilityFlagCount: Integer; diff --git a/source/units/Goccia.AST.BindingPatterns.pas b/source/units/Goccia.AST.BindingPatterns.pas index eb65ac02..7246571c 100644 --- a/source/units/Goccia.AST.BindingPatterns.pas +++ b/source/units/Goccia.AST.BindingPatterns.pas @@ -100,6 +100,7 @@ procedure CollectVarBindingNamesFromNode(const ANode: TGocciaASTNode; Block: TGocciaBlockStatement; IfStmt: TGocciaIfStatement; ForOf: TGocciaForOfStatement; + ForIn: TGocciaForInStatement; ForStmt: TGocciaForStatement; WhileStmt: TGocciaWhileStatement; DoWhileStmt: TGocciaDoWhileStatement; @@ -160,6 +161,18 @@ procedure CollectVarBindingNamesFromNode(const ANode: TGocciaASTNode; end; CollectVarBindingNamesFromNode(ForOf.Body, ANames); end + else if ANode is TGocciaForInStatement then + begin + ForIn := TGocciaForInStatement(ANode); + if ForIn.IsVar then + begin + if Assigned(ForIn.BindingPattern) then + CollectPatternBindingNames(ForIn.BindingPattern, ANames, True) + else + AddBindingName(ANames, ForIn.BindingName, True); + end; + CollectVarBindingNamesFromNode(ForIn.Body, ANames); + end else if ANode is TGocciaForStatement then begin ForStmt := TGocciaForStatement(ANode); diff --git a/source/units/Goccia.AST.Statements.pas b/source/units/Goccia.AST.Statements.pas index 447e102b..7c97a3dc 100644 --- a/source/units/Goccia.AST.Statements.pas +++ b/source/units/Goccia.AST.Statements.pas @@ -179,6 +179,31 @@ TGocciaForOfStatement = class(TGocciaStatement) property Body: TGocciaStatement read FBody; end; + TGocciaForInStatement = class(TGocciaStatement) + private + FIsConst: Boolean; + FIsVar: Boolean; + FBindingName: string; + FBindingPattern: TGocciaDestructuringPattern; + FAssignmentTarget: TGocciaDestructuringPattern; + FObjectExpression: TGocciaExpression; + FBody: TGocciaStatement; + public + constructor Create(const AIsConst: Boolean; const ABindingName: string; + const ABindingPattern: TGocciaDestructuringPattern; + const AObjectExpression: TGocciaExpression; const ABody: TGocciaStatement; + const ALine, AColumn: Integer; + const AAssignmentTarget: TGocciaDestructuringPattern = nil); + function Execute(const AContext: TGocciaEvaluationContext): TGocciaControlFlow; override; + property IsConst: Boolean read FIsConst; + property IsVar: Boolean read FIsVar write FIsVar; + property BindingName: string read FBindingName; + property BindingPattern: TGocciaDestructuringPattern read FBindingPattern; + property AssignmentTarget: TGocciaDestructuringPattern read FAssignmentTarget; + property ObjectExpression: TGocciaExpression read FObjectExpression; + property Body: TGocciaStatement read FBody; + end; + TGocciaForAwaitOfStatement = class(TGocciaForOfStatement) public function Execute(const AContext: TGocciaEvaluationContext): TGocciaControlFlow; override; @@ -790,6 +815,24 @@ function HasUseStrictDirective(const AProgram: TGocciaProgram): Boolean; FBody := ABody; end; +{ TGocciaForInStatement } + +constructor TGocciaForInStatement.Create(const AIsConst: Boolean; + const ABindingName: string; + const ABindingPattern: TGocciaDestructuringPattern; + const AObjectExpression: TGocciaExpression; const ABody: TGocciaStatement; + const ALine, AColumn: Integer; + const AAssignmentTarget: TGocciaDestructuringPattern = nil); +begin + inherited Create(ALine, AColumn); + FIsConst := AIsConst; + FBindingName := ABindingName; + FBindingPattern := ABindingPattern; + FAssignmentTarget := AAssignmentTarget; + FObjectExpression := AObjectExpression; + FBody := ABody; +end; + { TGocciaReturnStatement } constructor TGocciaReturnStatement.Create(const AValue: TGocciaExpression; @@ -1192,6 +1235,11 @@ function HasUseStrictDirective(const AProgram: TGocciaProgram): Boolean; Result := EvaluateForOf(Self, AContext); end; + function TGocciaForInStatement.Execute(const AContext: TGocciaEvaluationContext): TGocciaControlFlow; + begin + Result := EvaluateForIn(Self, AContext); + end; + function TGocciaForAwaitOfStatement.Execute(const AContext: TGocciaEvaluationContext): TGocciaControlFlow; begin Result := EvaluateForAwaitOf(Self, AContext); diff --git a/source/units/Goccia.Bytecode.OpCodeNames.pas b/source/units/Goccia.Bytecode.OpCodeNames.pas index 6a1196dc..d4155b0b 100644 --- a/source/units/Goccia.Bytecode.OpCodeNames.pas +++ b/source/units/Goccia.Bytecode.OpCodeNames.pas @@ -173,6 +173,8 @@ function OpCodeName(const AOp: UInt8): string; OP_TO_OBJECT: Result := 'OP_TO_OBJECT'; OP_HAS_WITH_BINDING: Result := 'OP_HAS_WITH_BINDING'; OP_TO_PROPERTY_KEY: Result := 'OP_TO_PROPERTY_KEY'; + OP_ENUM_KEYS: Result := 'OP_ENUM_KEYS'; + OP_ENUM_ENTRY: Result := 'OP_ENUM_ENTRY'; OP_INC: Result := 'OP_INC'; OP_DEC: Result := 'OP_DEC'; OP_TO_NUMERIC: Result := 'OP_TO_NUMERIC'; diff --git a/source/units/Goccia.Bytecode.pas b/source/units/Goccia.Bytecode.pas index 4cd2173e..cd63c6a3 100644 --- a/source/units/Goccia.Bytecode.pas +++ b/source/units/Goccia.Bytecode.pas @@ -63,7 +63,10 @@ interface // definitions can reuse source-order property keys. // v36 -> v37: added OP_SETUP_AUTO_ACCESSOR_DYNAMIC for computed // auto-accessor keys. - GOCCIA_FORMAT_VERSION = 37; + // v37 -> v38: added OP_ENUM_KEYS for --compat-for-in-loop bytecode. + // v38 -> v39: OP_ENUM_KEYS now creates revalidating for-in entry arrays, + // and OP_ENUM_ENTRY validates entries. + GOCCIA_FORMAT_VERSION = 39; GOCCIA_BINARY_MAGIC: array[0..3] of Byte = (Ord('G'), Ord('B'), Ord('C'), 0); GOCCIA_NULLISH_MATCH_UNDEFINED = 0; GOCCIA_NULLISH_MATCH_NULL = 1; @@ -260,7 +263,9 @@ interface OP_CREATE_ARGUMENTS = 182, OP_TO_OBJECT = 183, OP_HAS_WITH_BINDING = 184, - OP_TO_PROPERTY_KEY = 185 + OP_TO_PROPERTY_KEY = 185, + OP_ENUM_KEYS = 186, + OP_ENUM_ENTRY = 187 ); function EncodeABC(const AOp: TGocciaOpCode; const A, B, C: UInt8): UInt32; inline; diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index e56db256..8731f680 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -35,6 +35,8 @@ procedure CompileTryStatement(const ACtx: TGocciaCompilationContext; const AStmt: TGocciaTryStatement); procedure CompileForOfStatement(const ACtx: TGocciaCompilationContext; const AStmt: TGocciaForOfStatement); +procedure CompileForInStatement(const ACtx: TGocciaCompilationContext; + const AStmt: TGocciaForInStatement); procedure CompileForAwaitOfStatement(const ACtx: TGocciaCompilationContext; const AStmt: TGocciaForAwaitOfStatement); procedure CompileForStatement(const ACtx: TGocciaCompilationContext; @@ -247,6 +249,7 @@ function StatementIsIteration(const AStmt: TGocciaStatement): Boolean; begin Result := (AStmt is TGocciaForStatement) or (AStmt is TGocciaForOfStatement) or + (AStmt is TGocciaForInStatement) or (AStmt is TGocciaForAwaitOfStatement) or (AStmt is TGocciaWhileStatement) or (AStmt is TGocciaDoWhileStatement); @@ -1474,6 +1477,9 @@ function StatementNeedsIteratorClose(const AStmt: TGocciaStatement): Boolean; if AStmt is TGocciaForOfStatement then Exit(True); + if AStmt is TGocciaForInStatement then + Exit(True); + if AStmt is TGocciaForStatement then Exit(True); @@ -2221,6 +2227,96 @@ procedure CompileForOfStatement(const ACtx: TGocciaCompilationContext; ACtx.Scope.FreeRegister; end; +procedure CompileForInStatement(const ACtx: TGocciaCompilationContext; + const AStmt: TGocciaForInStatement); +var + EntriesReg, LenReg, IdxReg, OneReg, CmpReg, EntryReg, KeyReg, + ValidReg: UInt8; + LoopStart, ExitJump, I: Integer; + Slot: UInt8; + ClosedLocals: array[0..255] of UInt8; + ClosedCount: Integer; + LoopControl: TLoopControlState; +begin + EntriesReg := ACtx.Scope.AllocateRegister; + LenReg := ACtx.Scope.AllocateRegister; + IdxReg := ACtx.Scope.AllocateRegister; + OneReg := ACtx.Scope.AllocateRegister; + CmpReg := ACtx.Scope.AllocateRegister; + EntryReg := ACtx.Scope.AllocateRegister; + KeyReg := ACtx.Scope.AllocateRegister; + ValidReg := ACtx.Scope.AllocateRegister; + + ACtx.CompileExpression(AStmt.ObjectExpression, EntriesReg); + EmitInstruction(ACtx, EncodeABC(OP_ENUM_KEYS, EntriesReg, EntriesReg, 0)); + EmitInstruction(ACtx, EncodeABC(OP_GET_LENGTH, LenReg, EntriesReg, 0)); + EmitInstruction(ACtx, EncodeAsBx(OP_LOAD_INT, IdxReg, 0)); + EmitInstruction(ACtx, EncodeAsBx(OP_LOAD_INT, OneReg, 1)); + + BeginLoopControl(ACtx, LoopControl); + try + LoopStart := CurrentCodePosition(ACtx); + EmitInstruction(ACtx, EncodeABC(OP_GTE_INT, CmpReg, IdxReg, LenReg)); + ExitJump := EmitJumpInstruction(ACtx, OP_JUMP_IF_TRUE, CmpReg); + EmitInstruction(ACtx, EncodeABC(OP_ARRAY_GET, EntryReg, EntriesReg, IdxReg)); + EmitInstruction(ACtx, EncodeABC(OP_ADD_INT, IdxReg, IdxReg, OneReg)); + EmitInstruction(ACtx, EncodeABC(OP_ENUM_ENTRY, KeyReg, ValidReg, EntryReg)); + EmitInstruction(ACtx, + EncodeAsBx(OP_JUMP_IF_FALSE, ValidReg, + LoopStart - CurrentCodePosition(ACtx) - 1)); + + ACtx.Scope.BeginScope; + SetLoopContinueScopeDepth(ACtx); + SetLabeledContinueCleanupBase(AStmt); + + if Assigned(AStmt.AssignmentTarget) then + EmitDestructuring(ACtx, AStmt.AssignmentTarget, KeyReg, True) + else if Assigned(AStmt.BindingPattern) then + begin + if AStmt.IsVar then + CollectDestructuringVarBindings(AStmt.BindingPattern, ACtx.Scope) + else + CollectDestructuringBindings(AStmt.BindingPattern, ACtx.Scope, + AStmt.IsConst); + EmitDestructuring(ACtx, AStmt.BindingPattern, KeyReg); + end + else if AStmt.BindingName <> '' then + begin + if AStmt.IsVar then + Slot := ACtx.Scope.DeclareVarLocal(AStmt.BindingName) + else + Slot := ACtx.Scope.DeclareLocal(AStmt.BindingName, AStmt.IsConst); + EmitInstruction(ACtx, EncodeABC(OP_MOVE, Slot, KeyReg, 0)); + end; + + ACtx.CompileStatement(AStmt.Body); + + PatchJumpList(ACtx, LoopControl.ContinueJumps); + PatchLabeledContinueJumps(ACtx, AStmt); + + ACtx.Scope.EndScope(ClosedLocals, ClosedCount); + for I := 0 to ClosedCount - 1 do + EmitInstruction(ACtx, EncodeABC(OP_CLOSE_UPVALUE, ClosedLocals[I], 0, 0)); + + EmitInstruction(ACtx, + EncodeAx(OP_JUMP, LoopStart - CurrentCodePosition(ACtx) - 1)); + + PatchJumpTarget(ACtx, ExitJump); + PatchJumpList(ACtx, LoopControl.BreakJumps); + finally + EndLoopControl(LoopControl); + end; + + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; + ACtx.Scope.FreeRegister; +end; + procedure CompileForAwaitOfStatement(const ACtx: TGocciaCompilationContext; const AStmt: TGocciaForAwaitOfStatement); var @@ -2515,6 +2611,7 @@ function ForBodyAssignsIdentifier(const ANode: TGocciaASTNode; Block: TGocciaBlockStatement; IfStmt: TGocciaIfStatement; ForOf: TGocciaForOfStatement; + ForIn: TGocciaForInStatement; ForStmt: TGocciaForStatement; WhileStmt: TGocciaWhileStatement; DoWhileStmt: TGocciaDoWhileStatement; @@ -2581,6 +2678,14 @@ function ForBodyAssignsIdentifier(const ANode: TGocciaASTNode; if ForBodyAssignsIdentifier(ForOf.Iterable, AName) then Exit(True); Result := ForBodyAssignsIdentifier(ForOf.Body, AName); end + else if ANode is TGocciaForInStatement then + begin + ForIn := TGocciaForInStatement(ANode); + if (ForIn.BindingName = AName) and ForIn.IsVar then + Exit(True); + if ForBodyAssignsIdentifier(ForIn.ObjectExpression, AName) then Exit(True); + Result := ForBodyAssignsIdentifier(ForIn.Body, AName); + end else if ANode is TGocciaForStatement then begin ForStmt := TGocciaForStatement(ANode); diff --git a/source/units/Goccia.Compiler.pas b/source/units/Goccia.Compiler.pas index 641570e5..87de01c3 100644 --- a/source/units/Goccia.Compiler.pas +++ b/source/units/Goccia.Compiler.pas @@ -275,6 +275,9 @@ function TGocciaCompiler.DoCompileStatement(const AStmt: TGocciaStatement): Bool Goccia.Compiler.Statements.CompileTryStatement(Ctx, TGocciaTryStatement(AStmt)) else if AStmt is TGocciaForAwaitOfStatement then Goccia.Compiler.Statements.CompileForAwaitOfStatement(Ctx, TGocciaForAwaitOfStatement(AStmt)) + else if AStmt is TGocciaForInStatement then + Goccia.Compiler.Statements.CompileForInStatement(Ctx, + TGocciaForInStatement(AStmt)) else if AStmt is TGocciaForOfStatement then Goccia.Compiler.Statements.CompileForOfStatement(Ctx, TGocciaForOfStatement(AStmt)) else if AStmt is TGocciaForStatement then @@ -546,6 +549,7 @@ procedure HoistVarLocals(const ANode: TGocciaASTNode; const AScope: TGocciaCompi Block: TGocciaBlockStatement; IfStmt: TGocciaIfStatement; ForOf: TGocciaForOfStatement; + ForIn: TGocciaForInStatement; ForStmt: TGocciaForStatement; WhileStmt: TGocciaWhileStatement; DoWhileStmt: TGocciaDoWhileStatement; @@ -592,6 +596,18 @@ procedure HoistVarLocals(const ANode: TGocciaASTNode; const AScope: TGocciaCompi ForOf := TGocciaForOfStatement(ANode); HoistVarLocals(ForOf.Body, AScope); end + else if ANode is TGocciaForInStatement then + begin + ForIn := TGocciaForInStatement(ANode); + if ForIn.IsVar then + begin + if Assigned(ForIn.BindingPattern) then + CollectDestructuringVarBindings(ForIn.BindingPattern, AScope) + else if ForIn.BindingName <> '' then + AScope.DeclareVarLocal(ForIn.BindingName); + end; + HoistVarLocals(ForIn.Body, AScope); + end else if ANode is TGocciaForStatement then begin ForStmt := TGocciaForStatement(ANode); diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 592b9bd6..50422c4f 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -65,6 +65,7 @@ function EvaluateAwait(const AAwaitExpression: TGocciaAwaitExpression; const ACo function EvaluateYield(const AYieldExpression: TGocciaYieldExpression; const AContext: TGocciaEvaluationContext): TGocciaValue; function EvaluateUsingDeclaration(const AUsingDeclaration: TGocciaUsingDeclaration; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; function EvaluateForOf(const AForOfStatement: TGocciaForOfStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; +function EvaluateForIn(const AForInStatement: TGocciaForInStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; function EvaluateForAwaitOf(const AForAwaitOfStatement: TGocciaForAwaitOfStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; function EvaluateFor(const AForStatement: TGocciaForStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; function EvaluateWhile(const AWhileStatement: TGocciaWhileStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; @@ -144,12 +145,17 @@ implementation Goccia.Values.ProxyValue, Goccia.Values.SetValue, Goccia.Values.SymbolValue, + Goccia.Values.ToObject, Goccia.Values.ToPrimitive; procedure RunClassInstanceInitializers(const AClassValue: TGocciaClassValue; const AInstance: TGocciaObjectValue; const AContext: TGocciaEvaluationContext); forward; +const + FOR_IN_ENTRY_OWNER = '__gocciaForInOwner'; + FOR_IN_ENTRY_KEY = '__gocciaForInKey'; + // Helper: create a non-owning copy of a statement list (AST owns the nodes) function CopyStatementList(const ASource: TObjectList): TObjectList; var @@ -1894,6 +1900,233 @@ function EvaluateForOf(const AForOfStatement: TGocciaForOfStatement; const ACont end; end; +function CreateForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue; +var + Current, Obj, EntryObj: TGocciaObjectValue; + Keys: TArray; + Key: string; + Visited: TStringList; +begin + Result := TGocciaArrayValue.Create; + if (AValue is TGocciaUndefinedLiteralValue) or + (AValue is TGocciaNullLiteralValue) then + Exit; + + Obj := ToObject(AValue); + Visited := TStringList.Create; + try + Visited.CaseSensitive := True; + Current := Obj; + while Assigned(Current) do + begin + Keys := Current.GetAllPropertyNames; + for Key in Keys do + begin + if Visited.IndexOf(Key) >= 0 then + Continue; + + Visited.Add(Key); + EntryObj := TGocciaObjectValue.Create; + EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, + TGocciaPropertyDescriptorData.Create(Current, [])); + EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, + TGocciaPropertyDescriptorData.Create( + TGocciaStringLiteralValue.Create(Key), [])); + Result.Elements.Add(EntryObj); + end; + Current := Current.Prototype; + end; + finally + Visited.Free; + end; +end; + +function TryForInEntryKey(const AEntry: TGocciaValue; out AKey: string): Boolean; +var + EntryObj, Owner: TGocciaObjectValue; + OwnerValue, KeyValue: TGocciaValue; + Descriptor: TGocciaPropertyDescriptor; +begin + AKey := ''; + if not (AEntry is TGocciaObjectValue) then + Exit(False); + + EntryObj := TGocciaObjectValue(AEntry); + OwnerValue := EntryObj.GetProperty(FOR_IN_ENTRY_OWNER); + KeyValue := EntryObj.GetProperty(FOR_IN_ENTRY_KEY); + if not (OwnerValue is TGocciaObjectValue) or + not (KeyValue is TGocciaStringLiteralValue) then + Exit(False); + + Owner := TGocciaObjectValue(OwnerValue); + AKey := TGocciaStringLiteralValue(KeyValue).Value; + Descriptor := Owner.GetOwnPropertyDescriptor(AKey); + Result := Assigned(Descriptor) and Descriptor.Enumerable; +end; + +// ES2026 §14.7.5.5 Runtime Semantics: ForInOfLoopEvaluation +function EvaluateForIn(const AForInStatement: TGocciaForInStatement; + const AContext: TGocciaEvaluationContext): TGocciaControlFlow; +var + SourceValue: TGocciaValue; + EntriesArray: TGocciaArrayValue; + EntryIndex: Integer; + EntryValue: TGocciaValue; + CurrentValue: TGocciaValue; + CurrentKey: string; + FoundKey: Boolean; + CF: TGocciaControlFlow; + IterScope: TGocciaScope; + IterContext: TGocciaEvaluationContext; + DeclarationType: TGocciaDeclarationType; + Continuation: TGocciaGeneratorContinuation; + SavedIteratorValue, SavedCurrentValue, SavedNextMethod: TGocciaValue; + SavedIterScope, SavedActiveScope: TGocciaScope; + HasSavedLoopState: Boolean; +begin + Result := TGocciaControlFlow.Normal(TGocciaUndefinedLiteralValue.UndefinedValue); + + Continuation := CurrentGeneratorContinuation; + HasSavedLoopState := Assigned(Continuation) and + Continuation.GetLoopState(AForInStatement, SavedIteratorValue, + SavedCurrentValue, SavedNextMethod, SavedIterScope, SavedActiveScope); + if HasSavedLoopState then + begin + EntriesArray := TGocciaArrayValue(SavedIteratorValue); + EntryIndex := 0; + if SavedNextMethod is TGocciaNumberLiteralValue then + EntryIndex := Trunc(TGocciaNumberLiteralValue(SavedNextMethod).Value); + end + else + begin + SourceValue := EvaluateExpression(AForInStatement.ObjectExpression, AContext); + EntriesArray := CreateForInEntriesArray(SourceValue); + EntryIndex := 0; + end; + + if AForInStatement.IsConst then + DeclarationType := dtConst + else + DeclarationType := dtLet; + + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.AddTempRoot(EntriesArray); + try + while True do + begin + IterScope := nil; + if HasSavedLoopState then + begin + CurrentValue := SavedCurrentValue; + IterScope := SavedIterScope; + HasSavedLoopState := False; + end + else + begin + CurrentKey := ''; + FoundKey := False; + while EntryIndex < EntriesArray.Elements.Count do + begin + EntryValue := EntriesArray.Elements[EntryIndex]; + Inc(EntryIndex); + if TryForInEntryKey(EntryValue, CurrentKey) then + begin + FoundKey := True; + Break; + end; + end; + if not FoundKey then + Break; + CurrentValue := TGocciaStringLiteralValue.Create(CurrentKey); + end; + + CheckExecutionTimeout; + IncrementInstructionCounter; + CheckInstructionLimit; + + if Assigned(IterScope) then + begin + IterContext := AContext; + if Assigned(SavedActiveScope) then + IterContext.Scope := SavedActiveScope + else + IterContext.Scope := IterScope; + end + else + begin + IterScope := AContext.Scope.CreateChild(skBlock); + IterContext := AContext; + IterContext.Scope := IterScope; + + if AForInStatement.IsVar then + begin + if AForInStatement.BindingPattern <> nil then + AssignPattern(AForInStatement.BindingPattern, CurrentValue, + IterContext) + else + AContext.Scope.DefineVariableBinding( + AForInStatement.BindingName, CurrentValue, True); + end + else if AForInStatement.AssignmentTarget <> nil then + AssignPattern(AForInStatement.AssignmentTarget, CurrentValue, + IterContext) + else if AForInStatement.BindingPattern <> nil then + AssignPattern(AForInStatement.BindingPattern, CurrentValue, + IterContext, True, DeclarationType) + else + IterScope.DefineLexicalBinding(AForInStatement.BindingName, + CurrentValue, DeclarationType); + + if Assigned(Continuation) then + Continuation.SaveLoopState(AForInStatement, EntriesArray, CurrentValue, + TGocciaNumberLiteralValue.Create(EntryIndex), IterScope, + IterContext.Scope); + end; + + try + CF := EvaluateLoopBodyStatement(AForInStatement.Body, IterContext); + if Assigned(Continuation) then + Continuation.ClearLoopState(AForInStatement); + except + on E: EGocciaGeneratorYield do + raise; + else + begin + if Assigned(Continuation) then + Continuation.ClearLoopState(AForInStatement); + raise; + end; + end; + + if CF.Kind = cfkBreak then + begin + if not TargetsStatementOrUnlabeled(CF, AForInStatement) then + begin + Result := CF; + Exit; + end; + Break; + end; + if CF.Kind = cfkReturn then + begin + Result := CF; + Exit; + end; + if (CF.Kind = cfkContinue) and + not TargetsStatementOrUnlabeled(CF, AForInStatement) then + begin + Result := CF; + Exit; + end; + end; + if Assigned(Continuation) then + Continuation.ClearLoopState(AForInStatement); + finally + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.RemoveTempRoot(EntriesArray); + end; +end; + // ES2026 §14.7.4.2 ForBodyEvaluation — traditional for(init; test; update) loop function EvaluateFor(const AForStatement: TGocciaForStatement; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; @@ -6554,6 +6787,7 @@ procedure AssignVariablePattern(const APattern: TGocciaDestructuringPattern; con ArrPat: TGocciaArrayDestructuringPattern; AssignPat: TGocciaAssignmentDestructuringPattern; RestPat: TGocciaRestDestructuringPattern; + ObjectValue: TGocciaObjectValue; ArrayValue: TGocciaArrayValue; Iterator: TGocciaIteratorValue; IterResult: TGocciaObjectValue; @@ -6572,6 +6806,7 @@ procedure AssignVariablePattern(const APattern: TGocciaDestructuringPattern; con ThrowTypeError( Format(SErrorCannotDestructure, [AValue.ToStringLiteral.Value]), SSuggestDestructureRequiresObject); + ObjectValue := ToObject(AValue); ObjPat := TGocciaObjectDestructuringPattern(APattern); for I := 0 to ObjPat.Properties.Count - 1 do begin @@ -6583,18 +6818,18 @@ procedure AssignVariablePattern(const APattern: TGocciaDestructuringPattern; con if PropertyKey is TGocciaSymbolValue then begin // Keep the class dispatch explicit to mirror assignment patterns. - if AValue is TGocciaClassValue then - PropValue := TGocciaClassValue(AValue).GetSymbolProperty(TGocciaSymbolValue(PropertyKey)) + if ObjectValue is TGocciaClassValue then + PropValue := TGocciaClassValue(ObjectValue).GetSymbolProperty(TGocciaSymbolValue(PropertyKey)) else - PropValue := (AValue as TGocciaObjectValue).GetSymbolProperty(TGocciaSymbolValue(PropertyKey)); + PropValue := ObjectValue.GetSymbolProperty(TGocciaSymbolValue(PropertyKey)); end else - PropValue := (AValue as TGocciaObjectValue).GetProperty(TGocciaStringLiteralValue(PropertyKey).Value); + PropValue := ObjectValue.GetProperty(TGocciaStringLiteralValue(PropertyKey).Value); if not Assigned(PropValue) then PropValue := TGocciaUndefinedLiteralValue.UndefinedValue; end else - PropValue := (AValue as TGocciaObjectValue).GetProperty(ObjPat.Properties[I].Key); + PropValue := ObjectValue.GetProperty(ObjPat.Properties[I].Key); AssignVariablePattern(ObjPat.Properties[I].Pattern, PropValue, AContext); end; end @@ -7039,20 +7274,13 @@ procedure AssignObjectPattern(const APattern: TGocciaObjectDestructuringPattern; ObjectPair: TPair; I: Integer; begin - // Check if value is an object - if not (AValue is TGocciaObjectValue) then - begin - if (AValue is TGocciaNullLiteralValue) or (AValue is TGocciaUndefinedLiteralValue) then - ThrowTypeError( - Format(SErrorCannotDestructure, [AValue.ToStringLiteral.Value]), - SSuggestDestructureRequiresObject) - else - ThrowTypeError( - Format(SErrorCannotDestructureType, [AValue.TypeName]), - SSuggestDestructureRequiresObject); - end; + if (AValue is TGocciaNullLiteralValue) or + (AValue is TGocciaUndefinedLiteralValue) then + ThrowTypeError( + Format(SErrorCannotDestructure, [AValue.ToStringLiteral.Value]), + SSuggestDestructureRequiresObject); - ObjectValue := TGocciaObjectValue(AValue); + ObjectValue := ToObject(AValue); UsedKeys := TStringList.Create; try diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index 6b3c2fb4..fb96b706 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -35,6 +35,7 @@ TGocciaParserOptions = record LooseEqualityEnabled: Boolean; NonStrictModeEnabled: Boolean; LabelStatementsEnabled: Boolean; + ForInLoopsEnabled: Boolean; end; TGocciaPrivateNameReference = record @@ -90,6 +91,7 @@ TGocciaParser = class FLooseEqualityEnabled: Boolean; FNonStrictModeEnabled: Boolean; FLabelStatementsEnabled: Boolean; + FForInLoopsEnabled: Boolean; FStrictModeActive: Boolean; procedure AddWarning(const AMessage, ASuggestion: string; const ALine, AColumn: Integer); @@ -364,6 +366,7 @@ procedure TGocciaParser.ApplyOptions(const AOptions: TGocciaParserOptions); FLooseEqualityEnabled := AOptions.LooseEqualityEnabled; FNonStrictModeEnabled := AOptions.NonStrictModeEnabled; FLabelStatementsEnabled := AOptions.LabelStatementsEnabled; + FForInLoopsEnabled := AOptions.ForInLoopsEnabled; end; function TGocciaParser.Options: TGocciaParserOptions; @@ -376,6 +379,7 @@ function TGocciaParser.Options: TGocciaParserOptions; Result.LooseEqualityEnabled := FLooseEqualityEnabled; Result.NonStrictModeEnabled := FNonStrictModeEnabled; Result.LabelStatementsEnabled := FLabelStatementsEnabled; + Result.ForInLoopsEnabled := FForInLoopsEnabled; end; destructor TGocciaParser.Destroy; @@ -4309,9 +4313,11 @@ function TGocciaParser.ForStatement: TGocciaStatement; IsVar: Boolean; BindingName: string; BindingPattern: TGocciaDestructuringPattern; + AssignmentTarget: TGocciaDestructuringPattern; MatchPattern: TGocciaMatchPattern; IterableExpr: TGocciaExpression; BodyStmt: TGocciaStatement; + TargetExpr: TGocciaExpression; SavedCurrent: Integer; Token: TGocciaToken; begin @@ -4321,8 +4327,10 @@ function TGocciaParser.ForStatement: TGocciaStatement; // Try to parse for...of / for-await-of SavedCurrent := FCurrent; IsAwait := False; + IsConst := False; IsVar := False; BindingPattern := nil; + AssignmentTarget := nil; MatchPattern := nil; // for-await-of: 'await' appears between 'for' and '(' (async or top-level) @@ -4398,6 +4406,38 @@ function TGocciaParser.ForStatement: TGocciaStatement; 'Keyword must not contain escaped characters', Peek.Line, Peek.Column, FFileName, FSourceLines); + if Check(gttIn) then + begin + if IsAwait then + raise TGocciaSyntaxError.Create( + '''for await...in'' is not valid JavaScript syntax', + Line, Column, FFileName, FSourceLines, + 'Use ''for await...of'' for async iteration or ''for...in'' for property enumeration'); + + if not FForInLoopsEnabled then + begin + AddWarning('''for...in'' loops are not supported by default in GocciaScript', + 'Use Object.keys()/Object.entries() with for...of, or enable --compat-for-in-loop', + Line, Column); + FCurrent := SavedCurrent; + SkipBalancedParens; + SkipStatementOrBlock; + Result := TGocciaEmptyStatement.Create(Line, Column); + Exit; + end; + + Advance; // consume 'in' + IterableExpr := Expression; + Consume(gttRightParen, 'Expected ")" after for...in expression', + SSuggestCloseParenForOf); + + BodyStmt := IterationBodyStatement; + Result := TGocciaForInStatement.Create(IsConst, BindingName, + BindingPattern, IterableExpr, BodyStmt, Line, Column); + TGocciaForInStatement(Result).IsVar := IsVar; + Exit; + end; + if CheckContextualKeyword(KEYWORD_OF) then begin Advance; // consume 'of' @@ -4420,6 +4460,41 @@ function TGocciaParser.ForStatement: TGocciaStatement; end; Exit; end; + end + else if not Check(gttSemicolon) then + begin + TargetExpr := Call; + if Check(gttIn) then + begin + if IsAwait then + raise TGocciaSyntaxError.Create( + '''for await...in'' is not valid JavaScript syntax', + Line, Column, FFileName, FSourceLines, + 'Use ''for await...of'' for async iteration or ''for...in'' for property enumeration'); + + if not FForInLoopsEnabled then + begin + AddWarning('''for...in'' loops are not supported by default in GocciaScript', + 'Use Object.keys()/Object.entries() with for...of, or enable --compat-for-in-loop', + Line, Column); + FCurrent := SavedCurrent; + SkipBalancedParens; + SkipStatementOrBlock; + Result := TGocciaEmptyStatement.Create(Line, Column); + Exit; + end; + + AssignmentTarget := ConvertToPattern(TargetExpr); + Advance; // consume 'in' + IterableExpr := Expression; + Consume(gttRightParen, 'Expected ")" after for...in expression', + SSuggestCloseParenForOf); + + BodyStmt := IterationBodyStatement; + Result := TGocciaForInStatement.Create(False, '', nil, + IterableExpr, BodyStmt, Line, Column, AssignmentTarget); + Exit; + end; end; // Not a for...of — fall back to traditional for loop (gated by flag) diff --git a/source/units/Goccia.SourcePipeline.pas b/source/units/Goccia.SourcePipeline.pas index 720e9efe..c9e161ef 100644 --- a/source/units/Goccia.SourcePipeline.pas +++ b/source/units/Goccia.SourcePipeline.pas @@ -17,7 +17,7 @@ interface TGocciaPreprocessors = set of TGocciaPreprocessor; TGocciaCompatibility = (cfASI, cfVar, cfFunction, cfTraditionalFor, - cfWhileLoops, cfLooseEquality, cfNonStrictMode, cfLabel); + cfWhileLoops, cfLooseEquality, cfNonStrictMode, cfLabel, cfForIn); TGocciaCompatibilityFlags = set of TGocciaCompatibility; TGocciaSourceType = (stScript, stModule); @@ -186,6 +186,7 @@ function ParserOptionsForSourcePipeline( Result.NonStrictModeEnabled := (cfNonStrictMode in AOptions.Compatibility) and (AOptions.SourceType <> stModule); Result.LabelStatementsEnabled := cfLabel in AOptions.Compatibility; + Result.ForInLoopsEnabled := cfForIn in AOptions.Compatibility; end; procedure ConfigureParser(const AParser: TGocciaParser; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 7b86478e..195cb1be 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -162,6 +162,8 @@ TGocciaVM = class const ASource: TGocciaValue); function ObjectRestValue(const ASource: TGocciaValue; const AExclusionKeys: TGocciaArrayValue): TGocciaObjectValue; + function ForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue; + function TryForInEntryKey(const AEntry: TGocciaValue; out AKey: string): Boolean; function GetIteratorValue(const AIterable: TGocciaValue; const ATryAsync: Boolean): TGocciaValue; function ConstructValue(const AConstructor: TGocciaValue; @@ -349,6 +351,8 @@ implementation const BYTECODE_PRIVATE_SLOT_PREFIX = '#slot:'; BYTECODE_PRIVATE_BRAND_PREFIX = '#brand:'; + FOR_IN_ENTRY_OWNER = '__gocciaForInOwner'; + FOR_IN_ENTRY_KEY = '__gocciaForInKey'; function IsBytecodePrivateKey(const AKey: string): Boolean; forward; function IsBytecodePrivateBrandKey(const AKey: string): Boolean; forward; @@ -4849,6 +4853,72 @@ function TGocciaVM.ObjectRestValue(const ASource: TGocciaValue; end; end; +function TGocciaVM.ForInEntriesArray( + const AValue: TGocciaValue): TGocciaArrayValue; +var + Current, Obj, EntryObj: TGocciaObjectValue; + Keys: TArray; + Key: string; + Visited: TStringList; +begin + Result := TGocciaArrayValue.Create; + if (AValue is TGocciaUndefinedLiteralValue) or + (AValue is TGocciaNullLiteralValue) then + Exit; + + Obj := ToObject(AValue); + Visited := TStringList.Create; + try + Visited.CaseSensitive := True; + Current := Obj; + while Assigned(Current) do + begin + Keys := Current.GetAllPropertyNames; + for Key in Keys do + begin + if Visited.IndexOf(Key) >= 0 then + Continue; + + Visited.Add(Key); + EntryObj := TGocciaObjectValue.Create; + EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, + TGocciaPropertyDescriptorData.Create(Current, [])); + EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, + TGocciaPropertyDescriptorData.Create( + TGocciaStringLiteralValue.Create(Key), [])); + Result.Elements.Add(EntryObj); + end; + Current := Current.Prototype; + end; + finally + Visited.Free; + end; +end; + +function TGocciaVM.TryForInEntryKey(const AEntry: TGocciaValue; + out AKey: string): Boolean; +var + EntryObj, Owner: TGocciaObjectValue; + OwnerValue, KeyValue: TGocciaValue; + Descriptor: TGocciaPropertyDescriptor; +begin + AKey := ''; + if not (AEntry is TGocciaObjectValue) then + Exit(False); + + EntryObj := TGocciaObjectValue(AEntry); + OwnerValue := EntryObj.GetProperty(FOR_IN_ENTRY_OWNER); + KeyValue := EntryObj.GetProperty(FOR_IN_ENTRY_KEY); + if not (OwnerValue is TGocciaObjectValue) or + not (KeyValue is TGocciaStringLiteralValue) then + Exit(False); + + Owner := TGocciaObjectValue(OwnerValue); + AKey := TGocciaStringLiteralValue(KeyValue).Value; + Descriptor := Owner.GetOwnPropertyDescriptor(AKey); + Result := Assigned(Descriptor) and Descriptor.Enumerable; +end; + function TGocciaVM.GetIteratorValue(const AIterable: TGocciaValue; const ATryAsync: Boolean): TGocciaValue; var @@ -7650,6 +7720,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( DynImportPromise: TGocciaPromiseValue; SpreadArray: TGocciaArrayValue; RestoredContinuation: Boolean; + ForInKey: string; begin SavedRegisterBase := FRegisterBase; SavedRegisterCount := FRegisterCount; @@ -7786,6 +7857,24 @@ function TGocciaVM.ExecuteClosureRegistersInternal( OP_TO_PROPERTY_KEY: SetRegister(A, ToPropertyKey(RegisterToValue(FRegisters[B]))); + OP_ENUM_KEYS: + SetRegister(A, ForInEntriesArray(GetRegister(B))); + + OP_ENUM_ENTRY: + begin + if TryForInEntryKey(GetRegister(C), ForInKey) then + begin + FRegisters[A] := VMValueToRegisterFast( + TGocciaStringLiteralValue.Create(ForInKey)); + FRegisters[B] := RegisterBoolean(True); + end + else + begin + FRegisters[A] := RegisterUndefined; + FRegisters[B] := RegisterBoolean(False); + end; + end; + OP_LOAD_INT: FRegisters[A] := RegisterInt(DecodesBx(Instruction)); diff --git a/source/units/Goccia.Values.ObjectValue.pas b/source/units/Goccia.Values.ObjectValue.pas index a2f94189..8ba8b6fd 100644 --- a/source/units/Goccia.Values.ObjectValue.pas +++ b/source/units/Goccia.Values.ObjectValue.pas @@ -70,6 +70,7 @@ TGocciaObjectValue = class(TGocciaValue) function GetAllPropertyNames: TArray; virtual; function GetOwnPropertyNames: TArray; virtual; function GetOwnPropertyKeys: TArray; virtual; + function EnumerateForInPropertyNames: TArray; virtual; procedure DefineSymbolProperty(const ASymbol: TGocciaSymbolValue; const ADescriptor: TGocciaPropertyDescriptor); virtual; function TryDefineSymbolProperty(const ASymbol: TGocciaSymbolValue; const ADescriptor: TGocciaPropertyDescriptor): Boolean; virtual; @@ -113,6 +114,7 @@ TGocciaObjectValue = class(TGocciaValue) implementation uses + Classes, SysUtils, Goccia.Arithmetic, @@ -1097,6 +1099,47 @@ function TGocciaObjectValue.GetAllPropertyNames: TArray; Result := FProperties.Keys; end; +// ES2026 §14.7.5.9 EnumerateObjectProperties(O) +function TGocciaObjectValue.EnumerateForInPropertyNames: TArray; +var + Current: TGocciaObjectValue; + Keys: TArray; + Descriptor: TGocciaPropertyDescriptor; + Names: TList; + Visited: TStringList; + Key: string; +begin + Visited := TStringList.Create; + Names := TList.Create; + try + Visited.CaseSensitive := True; + Current := Self; + while Assigned(Current) do + begin + Keys := Current.GetAllPropertyNames; + for Key in Keys do + begin + if Visited.IndexOf(Key) >= 0 then + Continue; + + Descriptor := Current.GetOwnPropertyDescriptor(Key); + if not Assigned(Descriptor) then + Continue; + + Visited.Add(Key); + if Descriptor.Enumerable then + Names.Add(Key); + end; + Current := Current.Prototype; + end; + + Result := Names.ToArray; + finally + Names.Free; + Visited.Free; + end; +end; + { Symbol property methods } // ES2026 §20.1.2.3.1 DefinePropertyOrThrow — symbol variant diff --git a/tests/language/for-in-loop/basic-enumeration.js b/tests/language/for-in-loop/basic-enumeration.js new file mode 100644 index 00000000..8646c1aa --- /dev/null +++ b/tests/language/for-in-loop/basic-enumeration.js @@ -0,0 +1,114 @@ +/*--- +description: for-in loops enumerate object property names +features: [compat-for-in-loop] +---*/ + +test("enumerates enumerable own string keys", () => { + const obj = { a: 1, b: 2 }; + const keys = []; + for (const key in obj) keys.push(key); + expect(keys).toEqual(["a", "b"]); +}); + +test("assigns to an existing identifier target", () => { + let key; + const keys = []; + for (key in { a: 1, b: 2 }) { + keys.push(key); + } + expect(keys).toEqual(["a", "b"]); + expect(key).toBe("b"); +}); + +test("assigns to an existing member target", () => { + const state = { key: "" }; + const keys = []; + for (state.key in { a: 1, b: 2 }) { + keys.push(state.key); + } + expect(keys).toEqual(["a", "b"]); + expect(state.key).toBe("b"); +}); + +test("skips non-enumerable own properties", () => { + const obj = { visible: 1 }; + Object.defineProperty(obj, "hidden", { + value: 2, + enumerable: false, + }); + + const keys = []; + for (const key in obj) keys.push(key); + expect(keys).toEqual(["visible"]); +}); + +test("enumerates inherited enumerable string keys", () => { + const proto = { inherited: 1 }; + const obj = Object.create(proto); + obj.own = 2; + + const keys = []; + for (const key in obj) keys.push(key); + expect(keys).toEqual(["own", "inherited"]); +}); + +test("non-enumerable own properties shadow inherited enumerable keys", () => { + const proto = { shadowed: 1, inherited: 2 }; + const obj = Object.create(proto); + Object.defineProperty(obj, "shadowed", { + value: 3, + enumerable: false, + }); + + const keys = []; + for (const key in obj) keys.push(key); + expect(keys).toEqual(["inherited"]); +}); + +test("null and undefined right-hand sides are empty loops", () => { + const keys = []; + for (const key in null) keys.push(key); + for (const key in undefined) keys.push(key); + expect(keys).toEqual([]); +}); + +test("string primitives enumerate character indices", () => { + const keys = []; + for (const key in "abc") keys.push(key); + expect(keys).toEqual(["0", "1", "2"]); +}); + +test("deleted properties are not yielded when reached", () => { + const obj = { a: 1, b: 2 }; + const keys = []; + for (const key in obj) { + keys.push(key); + delete obj.b; + } + expect(keys).toEqual(["a"]); +}); + +test("properties made non-enumerable before visit are skipped", () => { + const obj = { a: 1, b: 2 }; + const keys = []; + for (const key in obj) { + keys.push(key); + Object.defineProperty(obj, "b", { enumerable: false }); + } + expect(keys).toEqual(["a"]); +}); + +test("deleted own shadow does not expose inherited property", () => { + const proto = { shadowed: 1 }; + const obj = Object.create(proto); + obj.a = 2; + obj.shadowed = 3; + + const keys = []; + for (const key in obj) { + keys.push(key); + delete obj.shadowed; + } + + expect(keys).toEqual(["a"]); +}); diff --git a/tests/language/for-in-loop/control-flow.js b/tests/language/for-in-loop/control-flow.js new file mode 100644 index 00000000..c49bbf52 --- /dev/null +++ b/tests/language/for-in-loop/control-flow.js @@ -0,0 +1,30 @@ +/*--- +description: break and continue in for-in loops +features: [compat-for-in-loop] +---*/ + +test("break exits the loop", () => { + const keys = []; + for (const key in { a: 1, b: 2, c: 3 }) { + if (key === "c") break; + keys.push(key); + } + expect(keys).toEqual(["a", "b"]); +}); + +test("continue skips the rest of the body", () => { + const keys = []; + for (const key in { a: 1, b: 2, c: 3 }) { + if (key === "b") continue; + keys.push(key); + } + expect(keys).toEqual(["a", "c"]); +}); + +test("closures capture per-iteration lexical bindings", () => { + const fns = []; + for (const key in { a: 1, b: 2, c: 3 }) { + fns.push(() => key); + } + expect(fns.map(fn => fn())).toEqual(["a", "b", "c"]); +}); diff --git a/tests/language/for-in-loop/goccia.json b/tests/language/for-in-loop/goccia.json new file mode 100644 index 00000000..4d373517 --- /dev/null +++ b/tests/language/for-in-loop/goccia.json @@ -0,0 +1,3 @@ +{ + "compat-for-in-loop": true +} diff --git a/tests/language/for-in-loop/var/goccia.json b/tests/language/for-in-loop/var/goccia.json new file mode 100644 index 00000000..83c2b0ca --- /dev/null +++ b/tests/language/for-in-loop/var/goccia.json @@ -0,0 +1,4 @@ +{ + "compat-for-in-loop": true, + "compat-var": true +} diff --git a/tests/language/for-in-loop/var/var-shared-binding.js b/tests/language/for-in-loop/var/var-shared-binding.js new file mode 100644 index 00000000..96183147 --- /dev/null +++ b/tests/language/for-in-loop/var/var-shared-binding.js @@ -0,0 +1,72 @@ +/*--- +description: var in for-in heads hoists out of the loop +features: [compat-for-in-loop, compat-var] +---*/ + +test("var in for-in is visible after the loop", () => { + for (var key in { a: 1, b: 2 }) {} + expect(key).toBe("b"); +}); + +test("var in for-in hoists into enclosing function", () => { + const f = () => { + for (var name in { first: 1 }) {} + return name; + }; + expect(f()).toBe("first"); +}); + +test("var captures a shared binding across iterations", () => { + const fns = []; + for (var key in { a: 1, b: 2 }) { + fns.push(() => key); + } + expect(fns[0]()).toBe("b"); + expect(fns[1]()).toBe("b"); +}); + +test("var array destructuring in for-in assigns hoisted bindings", () => { + const values = []; + for (var [x] in { xy: 1 }) { + values.push(x); + } + expect(values).toEqual(["x"]); + expect(x).toBe("x"); +}); + +test("var array destructuring in for-in is hoisted before the loop", () => { + expect(x).toBe(undefined); + for (var [x] in { xy: 1 }) {} + expect(x).toBe("x"); +}); + +test("var object destructuring in for-in assigns hoisted bindings", () => { + const values = []; + for (var { length: len, [0]: first } in { ab: 1, c: 2 }) { + values.push([len, first]); + } + expect(values).toEqual([[2, "a"], [1, "c"]]); + expect(len).toBe(1); + expect(first).toBe("c"); +}); + +test("var object destructuring in for-in supports computed names and defaults", () => { + const values = []; + var count = 0; + for (var { length: len, [len - 1 + count]: value = "fallback" } in "foo") { + values.push([len, value]); + count++; + } + expect(values).toEqual([[1, "0"], [1, "fallback"], [1, "fallback"]]); + expect(len).toBe(1); + expect(value).toBe("fallback"); +}); + +test("var destructuring captures a shared binding across iterations", () => { + const fns = []; + for (var [key] in { a: 1, b: 2 }) { + fns.push(() => key); + } + expect(fns[0]()).toBe("b"); + expect(fns[1]()).toBe("b"); +}); diff --git a/tests/language/statements/unsupported-features.js b/tests/language/statements/unsupported-features.js index 23a7ed51..b121a418 100644 --- a/tests/language/statements/unsupported-features.js +++ b/tests/language/statements/unsupported-features.js @@ -16,6 +16,20 @@ describe.runIf(hasGoccia)("unsupported features are skipped", () => { expect(x).toBe(1); }); + test("code after skipped for-in loop executes", () => { + let x = 1; + for (const key in { a: 1 }) { x = 99; } + expect(x).toBe(1); + }); + + test("code after skipped for-in assignment target loop executes", () => { + let x = 1; + let key; + for (key in { a: 1 }) { x = 99; } + expect(x).toBe(1); + expect(key).toBe(undefined); + }); + test("for loop with nested parentheses is skipped correctly", () => { let x = 1; for (let i = Math.max(0, 1); i < Math.min(10, 20); i++) { x = 99; } From 999ad2dd1f7f226a2ff6a3af98fea66384655e2e Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Tue, 2 Jun 2026 22:47:50 +0200 Subject: [PATCH 2/3] Fix for-in review findings --- source/units/Goccia.AST.Expressions.pas | 24 +++ source/units/Goccia.Compiler.Expressions.pas | 19 +- source/units/Goccia.Compiler.Statements.pas | 65 +++++- source/units/Goccia.Evaluator.pas | 186 ++++++++++++++---- source/units/Goccia.Parser.pas | 7 + source/units/Goccia.VM.pas | 84 +++++--- source/units/Goccia.Values.ObjectValue.pas | 42 ---- .../language/for-in-loop/basic-enumeration.js | 18 ++ .../traditional/for-in-mutates-counter.js | 18 ++ .../for-in-loop/traditional/goccia.json | 4 + .../for-in-loop/var/var-shared-binding.js | 25 +++ 11 files changed, 376 insertions(+), 116 deletions(-) create mode 100644 tests/language/for-in-loop/traditional/for-in-mutates-counter.js create mode 100644 tests/language/for-in-loop/traditional/goccia.json diff --git a/source/units/Goccia.AST.Expressions.pas b/source/units/Goccia.AST.Expressions.pas index 96f8dbdd..a3fcb5eb 100644 --- a/source/units/Goccia.AST.Expressions.pas +++ b/source/units/Goccia.AST.Expressions.pas @@ -23,6 +23,7 @@ interface TGocciaDestructuringPattern = class; TGocciaGetterExpression = class; TGocciaMatchPattern = class; + TGocciaPrivateMemberExpression = class; TGocciaSetterExpression = class; TGocciaParameter = record @@ -623,6 +624,16 @@ TGocciaMemberExpressionDestructuringPattern = class(TGocciaDestructuringPatter property Expression: TGocciaMemberExpression read FExpression; end; + // Private member expression pattern: this.#x + TGocciaPrivateMemberExpressionDestructuringPattern = class(TGocciaDestructuringPattern) + private + FExpression: TGocciaPrivateMemberExpression; + public + constructor Create(const AExpression: TGocciaPrivateMemberExpression; const ALine, AColumn: Integer); + function Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue; override; + property Expression: TGocciaPrivateMemberExpression read FExpression; + end; + // Destructuring assignment expression TGocciaDestructuringAssignmentExpression = class(TGocciaExpression) private @@ -1453,6 +1464,14 @@ constructor TGocciaMemberExpressionDestructuringPattern.Create(const AExpression FExpression := AExpression; end; +{ TGocciaPrivateMemberExpressionDestructuringPattern } + +constructor TGocciaPrivateMemberExpressionDestructuringPattern.Create(const AExpression: TGocciaPrivateMemberExpression; const ALine, AColumn: Integer); +begin + inherited Create(ALine, AColumn); + FExpression := AExpression; +end; + { TGocciaDestructuringAssignmentExpression } constructor TGocciaDestructuringAssignmentExpression.Create(const ALeft: TGocciaDestructuringPattern; const ARight: TGocciaExpression; const ALine, AColumn: Integer); @@ -2216,6 +2235,11 @@ function TGocciaMemberExpressionDestructuringPattern.Evaluate(const AContext: TG Result := TGocciaUndefinedLiteralValue.UndefinedValue; end; +function TGocciaPrivateMemberExpressionDestructuringPattern.Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue; +begin + Result := TGocciaUndefinedLiteralValue.UndefinedValue; +end; + function TGocciaDestructuringAssignmentExpression.Evaluate(const AContext: TGocciaEvaluationContext): TGocciaValue; begin Result := EvaluateDestructuringAssignment(Self, AContext); diff --git a/source/units/Goccia.Compiler.Expressions.pas b/source/units/Goccia.Compiler.Expressions.pas index f6c1b512..81dd3967 100644 --- a/source/units/Goccia.Compiler.Expressions.pas +++ b/source/units/Goccia.Compiler.Expressions.pas @@ -96,6 +96,9 @@ procedure EmitCreateArgumentsObject(const ACtx: TGocciaCompilationContext; const AArgumentsSlot: Integer); procedure CollectDestructuringBindings(const APattern: TGocciaDestructuringPattern; const AScope: TGocciaCompilerScope; const AIsConst: Boolean = False); +procedure EmitBindingAssignmentFromRegister(const ACtx: TGocciaCompilationContext; + const AName: string; const AValueReg: UInt8; + const AAssignmentMode: Boolean); procedure EmitDestructuring(const ACtx: TGocciaCompilationContext; const APattern: TGocciaDestructuringPattern; const ASrcReg: UInt8; const AAssignmentMode: Boolean = False); @@ -423,10 +426,6 @@ procedure CompileIdentifier(const ACtx: TGocciaCompilationContext; CompileIdentifierAccess(ACtx, AExpr, ADest, False); end; -procedure EmitBindingAssignmentFromRegister(const ACtx: TGocciaCompilationContext; - const AName: string; const AValueReg: UInt8; - const AAssignmentMode: Boolean); forward; - function HiddenWithBindingName(const AName: string): Boolean; begin Result := Pos('#with:', AName) = 1; @@ -1602,6 +1601,18 @@ procedure EmitDestructuring(const ACtx: TGocciaCompilationContext; ASrcReg); end; ACtx.Scope.FreeRegister; + end + else if APattern is TGocciaPrivateMemberExpressionDestructuringPattern then + begin + DestSlot := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression( + TGocciaPrivateMemberExpressionDestructuringPattern(APattern).Expression.ObjectExpr, + DestSlot); + EmitStorePropertyByName(ACtx, DestSlot, + PrivateKey(ACtx.Scope, + TGocciaPrivateMemberExpressionDestructuringPattern(APattern).Expression.PrivateName), + ASrcReg); + ACtx.Scope.FreeRegister; end; end; diff --git a/source/units/Goccia.Compiler.Statements.pas b/source/units/Goccia.Compiler.Statements.pas index 8731f680..444f701b 100644 --- a/source/units/Goccia.Compiler.Statements.pas +++ b/source/units/Goccia.Compiler.Statements.pas @@ -176,6 +176,10 @@ TLabelControlState = record IsIteration: Boolean; end; +procedure EmitGlobalDefinesForPattern(const ACtx: TGocciaCompilationContext; + const APattern: TGocciaDestructuringPattern; const AIsConst: Boolean; + const AIsVar: Boolean; const AHasInitializer: Boolean); forward; + threadvar GBreakJumps: TList; GContinueJumps: TList; @@ -2234,6 +2238,7 @@ procedure CompileForInStatement(const ACtx: TGocciaCompilationContext; ValidReg: UInt8; LoopStart, ExitJump, I: Integer; Slot: UInt8; + LocalIdx: Integer; ClosedLocals: array[0..255] of UInt8; ClosedCount: Integer; LoopControl: TLoopControlState; @@ -2247,6 +2252,24 @@ procedure CompileForInStatement(const ACtx: TGocciaCompilationContext; KeyReg := ACtx.Scope.AllocateRegister; ValidReg := ACtx.Scope.AllocateRegister; + if AStmt.IsVar and ACtx.GlobalBackedTopLevel then + begin + if Assigned(AStmt.BindingPattern) then + begin + CollectDestructuringVarBindings(AStmt.BindingPattern, ACtx.Scope); + EmitGlobalDefinesForPattern(ACtx, AStmt.BindingPattern, False, True, + False); + end + else if AStmt.BindingName <> '' then + begin + Slot := ACtx.Scope.DeclareVarLocal(AStmt.BindingName); + LocalIdx := FindLocalBySlot(ACtx.Scope, AStmt.BindingName, Slot); + if LocalIdx >= 0 then + ACtx.Scope.MarkGlobalBacked(LocalIdx); + EmitGlobalDefine(ACtx, Slot, AStmt.BindingName, False, True, False); + end; + end; + ACtx.CompileExpression(AStmt.ObjectExpression, EntriesReg); EmitInstruction(ACtx, EncodeABC(OP_ENUM_KEYS, EntriesReg, EntriesReg, 0)); EmitInstruction(ACtx, EncodeABC(OP_GET_LENGTH, LenReg, EntriesReg, 0)); @@ -2286,7 +2309,8 @@ procedure CompileForInStatement(const ACtx: TGocciaCompilationContext; Slot := ACtx.Scope.DeclareVarLocal(AStmt.BindingName) else Slot := ACtx.Scope.DeclareLocal(AStmt.BindingName, AStmt.IsConst); - EmitInstruction(ACtx, EncodeABC(OP_MOVE, Slot, KeyReg, 0)); + EmitBindingAssignmentFromRegister(ACtx, AStmt.BindingName, KeyReg, + False); end; ACtx.CompileStatement(AStmt.Body); @@ -2605,6 +2629,25 @@ function TryCompileCountedFor(const ACtx: TGocciaCompilationContext; Result := True; end; +function PatternAssignsIdentifier(const APattern: TGocciaDestructuringPattern; + const AName: string): Boolean; +var + Names: TStringList; +begin + Result := False; + if not Assigned(APattern) then + Exit; + + Names := TStringList.Create; + Names.CaseSensitive := True; + try + CollectPatternBindingNames(APattern, Names); + Result := Names.IndexOf(AName) >= 0; + finally + Names.Free; + end; +end; + function ForBodyAssignsIdentifier(const ANode: TGocciaASTNode; const AName: string): Boolean; var @@ -2673,16 +2716,28 @@ function ForBodyAssignsIdentifier(const ANode: TGocciaASTNode; else if ANode is TGocciaForOfStatement then begin ForOf := TGocciaForOfStatement(ANode); - if (ForOf.BindingName = AName) and ForOf.IsVar then - Exit(True); + if ForOf.IsVar then + begin + if ForOf.BindingName = AName then + Exit(True); + if PatternAssignsIdentifier(ForOf.BindingPattern, AName) then + Exit(True); + end; if ForBodyAssignsIdentifier(ForOf.Iterable, AName) then Exit(True); Result := ForBodyAssignsIdentifier(ForOf.Body, AName); end else if ANode is TGocciaForInStatement then begin ForIn := TGocciaForInStatement(ANode); - if (ForIn.BindingName = AName) and ForIn.IsVar then + if PatternAssignsIdentifier(ForIn.AssignmentTarget, AName) then Exit(True); + if ForIn.IsVar then + begin + if ForIn.BindingName = AName then + Exit(True); + if PatternAssignsIdentifier(ForIn.BindingPattern, AName) then + Exit(True); + end; if ForBodyAssignsIdentifier(ForIn.ObjectExpression, AName) then Exit(True); Result := ForBodyAssignsIdentifier(ForIn.Body, AName); end @@ -5147,7 +5202,7 @@ procedure CollectDestructuringVarBindings(const APattern: TGocciaDestructuringPa procedure EmitGlobalDefinesForPattern(const ACtx: TGocciaCompilationContext; const APattern: TGocciaDestructuringPattern; const AIsConst: Boolean; - const AIsVar: Boolean = False; const AHasInitializer: Boolean = True); + const AIsVar: Boolean; const AHasInitializer: Boolean); var ObjPat: TGocciaObjectDestructuringPattern; ArrPat: TGocciaArrayDestructuringPattern; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 50422c4f..a4b9bd70 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -155,6 +155,7 @@ procedure RunClassInstanceInitializers(const AClassValue: TGocciaClassValue; const FOR_IN_ENTRY_OWNER = '__gocciaForInOwner'; FOR_IN_ENTRY_KEY = '__gocciaForInKey'; + FOR_IN_MAX_PROTOTYPE_CHAIN_DEPTH = 256; // Helper: create a non-owning copy of a statement list (AST owns the nodes) function CopyStatementList(const ASource: TObjectList): TObjectList; @@ -1905,39 +1906,74 @@ function CreateForInEntriesArray(const AValue: TGocciaValue): TGocciaArrayValue; Current, Obj, EntryObj: TGocciaObjectValue; Keys: TArray; Key: string; + KeyValue: TGocciaStringLiteralValue; Visited: TStringList; + GC: TGarbageCollector; + ChainDepth: Integer; begin + GC := TGarbageCollector.Instance; Result := TGocciaArrayValue.Create; - if (AValue is TGocciaUndefinedLiteralValue) or - (AValue is TGocciaNullLiteralValue) then - Exit; - - Obj := ToObject(AValue); - Visited := TStringList.Create; + if Assigned(GC) then + GC.AddTempRoot(Result); try - Visited.CaseSensitive := True; - Current := Obj; - while Assigned(Current) do - begin - Keys := Current.GetAllPropertyNames; - for Key in Keys do + if (AValue is TGocciaUndefinedLiteralValue) or + (AValue is TGocciaNullLiteralValue) then + Exit; + + Obj := ToObject(AValue); + if Assigned(GC) then + GC.AddTempRoot(Obj); + Visited := TStringList.Create; + try + Visited.CaseSensitive := True; + Current := Obj; + ChainDepth := 0; + while Assigned(Current) do begin - if Visited.IndexOf(Key) >= 0 then - Continue; - - Visited.Add(Key); - EntryObj := TGocciaObjectValue.Create; - EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, - TGocciaPropertyDescriptorData.Create(Current, [])); - EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, - TGocciaPropertyDescriptorData.Create( - TGocciaStringLiteralValue.Create(Key), [])); - Result.Elements.Add(EntryObj); + Inc(ChainDepth); + if ChainDepth > FOR_IN_MAX_PROTOTYPE_CHAIN_DEPTH then + ThrowTypeError(Format(SErrorProtoChainDepthExceeded, ['for...in']), + SSuggestPrototypeChainTooDeep); + + Keys := Current.GetAllPropertyNames; + for Key in Keys do + begin + if Visited.IndexOf(Key) >= 0 then + Continue; + + Visited.Add(Key); + EntryObj := TGocciaObjectValue.Create; + if Assigned(GC) then + GC.AddTempRoot(EntryObj); + try + EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, + TGocciaPropertyDescriptorData.Create(Current, [])); + KeyValue := TGocciaStringLiteralValue.Create(Key); + if Assigned(GC) then + GC.AddTempRoot(KeyValue); + try + EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, + TGocciaPropertyDescriptorData.Create(KeyValue, [])); + finally + if Assigned(GC) then + GC.RemoveTempRoot(KeyValue); + end; + Result.Elements.Add(EntryObj); + finally + if Assigned(GC) then + GC.RemoveTempRoot(EntryObj); + end; + end; + Current := Current.Prototype; end; - Current := Current.Prototype; + finally + Visited.Free; + if Assigned(GC) then + GC.RemoveTempRoot(Obj); end; finally - Visited.Free; + if Assigned(GC) then + GC.RemoveTempRoot(Result); end; end; @@ -2058,24 +2094,40 @@ function EvaluateForIn(const AForInStatement: TGocciaForInStatement; IterContext := AContext; IterContext.Scope := IterScope; - if AForInStatement.IsVar then - begin - if AForInStatement.BindingPattern <> nil then - AssignPattern(AForInStatement.BindingPattern, CurrentValue, + if Assigned(Continuation) then + Continuation.SaveLoopState(AForInStatement, EntriesArray, + CurrentValue, TGocciaNumberLiteralValue.Create(EntryIndex), nil, + nil); + + try + if AForInStatement.IsVar then + begin + if AForInStatement.BindingPattern <> nil then + AssignPattern(AForInStatement.BindingPattern, CurrentValue, + IterContext) + else + AContext.Scope.DefineVariableBinding( + AForInStatement.BindingName, CurrentValue, True); + end + else if AForInStatement.AssignmentTarget <> nil then + AssignPattern(AForInStatement.AssignmentTarget, CurrentValue, IterContext) + else if AForInStatement.BindingPattern <> nil then + AssignPattern(AForInStatement.BindingPattern, CurrentValue, + IterContext, True, DeclarationType) else - AContext.Scope.DefineVariableBinding( - AForInStatement.BindingName, CurrentValue, True); - end - else if AForInStatement.AssignmentTarget <> nil then - AssignPattern(AForInStatement.AssignmentTarget, CurrentValue, - IterContext) - else if AForInStatement.BindingPattern <> nil then - AssignPattern(AForInStatement.BindingPattern, CurrentValue, - IterContext, True, DeclarationType) - else - IterScope.DefineLexicalBinding(AForInStatement.BindingName, - CurrentValue, DeclarationType); + IterScope.DefineLexicalBinding(AForInStatement.BindingName, + CurrentValue, DeclarationType); + except + on E: EGocciaGeneratorYield do + raise; + else + begin + if Assigned(Continuation) then + Continuation.ClearLoopState(AForInStatement); + raise; + end; + end; if Assigned(Continuation) then Continuation.SaveLoopState(AForInStatement, EntriesArray, CurrentValue, @@ -7042,12 +7094,64 @@ procedure AssignMemberExpressionPattern(const APattern: TGocciaMemberExpressionD APattern.Line, APattern.Column, AContext.NonStrictMode); end; +procedure AssignPrivateMemberExpressionPattern( + const APattern: TGocciaPrivateMemberExpressionDestructuringPattern; + const AValue: TGocciaValue; const AContext: TGocciaEvaluationContext); +var + ObjectValue: TGocciaValue; + Instance: TGocciaInstanceValue; + ClassValue: TGocciaClassValue; + AccessClass: TGocciaClassValue; + SetterFn: TGocciaFunctionBase; + SetterArgs: TGocciaArgumentsCollection; +begin + ObjectValue := EvaluateExpression(APattern.Expression.ObjectExpr, AContext); + + if ObjectValue is TGocciaInstanceValue then + begin + Instance := TGocciaInstanceValue(ObjectValue); + AccessClass := ResolveOwningClass(Instance, AContext); + + if AccessClass.HasPrivateSetter(APattern.Expression.PrivateName) then + begin + SetterFn := AccessClass.PrivatePropertySetter[APattern.Expression.PrivateName]; + SetterArgs := TGocciaArgumentsCollection.Create; + try + SetterArgs.Add(AValue); + SetterFn.Call(SetterArgs, Instance); + finally + SetterArgs.Free; + end; + end + else if AccessClass.HasPrivateGetter(APattern.Expression.PrivateName) then + ThrowPrivateSetterMissingError(APattern.Expression.PrivateName) + else + Instance.SetPrivateProperty(APattern.Expression.PrivateName, AValue, + AccessClass); + end + else if ObjectValue is TGocciaClassValue then + begin + ClassValue := TGocciaClassValue(ObjectValue); + AssignPrivateMemberOnClass(ClassValue, APattern.Expression.PrivateName, + AValue, AContext); + end + else if ObjectValue is TGocciaObjectValue then + AssignPrivateMemberOnObject(TGocciaObjectValue(ObjectValue), + APattern.Expression.PrivateName, AValue, AContext) + else + AContext.OnError( + Format('Private fields can only be assigned on class instances or classes, not %s', + [ObjectValue.TypeName]), APattern.Line, APattern.Column); +end; + procedure AssignPattern(const APattern: TGocciaDestructuringPattern; const AValue: TGocciaValue; const AContext: TGocciaEvaluationContext; const AIsDeclaration: Boolean = False; const ADeclarationType: TGocciaDeclarationType = dtLet); begin if APattern is TGocciaIdentifierDestructuringPattern then AssignIdentifierPattern(TGocciaIdentifierDestructuringPattern(APattern), AValue, AContext, AIsDeclaration, ADeclarationType) else if APattern is TGocciaMemberExpressionDestructuringPattern then AssignMemberExpressionPattern(TGocciaMemberExpressionDestructuringPattern(APattern), AValue, AContext) + else if APattern is TGocciaPrivateMemberExpressionDestructuringPattern then + AssignPrivateMemberExpressionPattern(TGocciaPrivateMemberExpressionDestructuringPattern(APattern), AValue, AContext) else if APattern is TGocciaArrayDestructuringPattern then AssignArrayPattern(TGocciaArrayDestructuringPattern(APattern), AValue, AContext, AIsDeclaration, ADeclarationType) else if APattern is TGocciaObjectDestructuringPattern then diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index fb96b706..94311dfd 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -6799,6 +6799,7 @@ function TGocciaParser.ConvertToPattern(const AExpr: TGocciaExpression): TGoccia ObjectExpr: TGocciaObjectExpression; IdentifierExpr: TGocciaIdentifierExpression; AssignmentExpr: TGocciaAssignmentExpression; + PrivateMemberExpr: TGocciaPrivateMemberExpression; Elements: TObjectList; Properties: TObjectList; I: Integer; @@ -6882,6 +6883,12 @@ function TGocciaParser.ConvertToPattern(const AExpr: TGocciaExpression): TGoccia Result := TGocciaMemberExpressionDestructuringPattern.Create( TGocciaMemberExpression(AExpr), AExpr.Line, AExpr.Column); end + else if AExpr is TGocciaPrivateMemberExpression then + begin + PrivateMemberExpr := TGocciaPrivateMemberExpression(AExpr); + Result := TGocciaPrivateMemberExpressionDestructuringPattern.Create( + PrivateMemberExpr, AExpr.Line, AExpr.Column); + end else if AExpr is TGocciaPropertyAssignmentExpression then begin // obj.prop = default -> member expression pattern with default value diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 195cb1be..187a7797 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -353,6 +353,7 @@ implementation BYTECODE_PRIVATE_BRAND_PREFIX = '#brand:'; FOR_IN_ENTRY_OWNER = '__gocciaForInOwner'; FOR_IN_ENTRY_KEY = '__gocciaForInKey'; + FOR_IN_MAX_PROTOTYPE_CHAIN_DEPTH = 256; function IsBytecodePrivateKey(const AKey: string): Boolean; forward; function IsBytecodePrivateBrandKey(const AKey: string): Boolean; forward; @@ -4859,39 +4860,74 @@ function TGocciaVM.ForInEntriesArray( Current, Obj, EntryObj: TGocciaObjectValue; Keys: TArray; Key: string; + KeyValue: TGocciaStringLiteralValue; Visited: TStringList; + GC: TGarbageCollector; + ChainDepth: Integer; begin + GC := TGarbageCollector.Instance; Result := TGocciaArrayValue.Create; - if (AValue is TGocciaUndefinedLiteralValue) or - (AValue is TGocciaNullLiteralValue) then - Exit; - - Obj := ToObject(AValue); - Visited := TStringList.Create; + if Assigned(GC) then + GC.AddTempRoot(Result); try - Visited.CaseSensitive := True; - Current := Obj; - while Assigned(Current) do - begin - Keys := Current.GetAllPropertyNames; - for Key in Keys do + if (AValue is TGocciaUndefinedLiteralValue) or + (AValue is TGocciaNullLiteralValue) then + Exit; + + Obj := ToObject(AValue); + if Assigned(GC) then + GC.AddTempRoot(Obj); + Visited := TStringList.Create; + try + Visited.CaseSensitive := True; + Current := Obj; + ChainDepth := 0; + while Assigned(Current) do begin - if Visited.IndexOf(Key) >= 0 then - Continue; + Inc(ChainDepth); + if ChainDepth > FOR_IN_MAX_PROTOTYPE_CHAIN_DEPTH then + ThrowTypeError(Format(SErrorProtoChainDepthExceeded, ['for...in']), + SSuggestPrototypeChainTooDeep); - Visited.Add(Key); - EntryObj := TGocciaObjectValue.Create; - EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, - TGocciaPropertyDescriptorData.Create(Current, [])); - EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, - TGocciaPropertyDescriptorData.Create( - TGocciaStringLiteralValue.Create(Key), [])); - Result.Elements.Add(EntryObj); + Keys := Current.GetAllPropertyNames; + for Key in Keys do + begin + if Visited.IndexOf(Key) >= 0 then + Continue; + + Visited.Add(Key); + EntryObj := TGocciaObjectValue.Create; + if Assigned(GC) then + GC.AddTempRoot(EntryObj); + try + EntryObj.DefineProperty(FOR_IN_ENTRY_OWNER, + TGocciaPropertyDescriptorData.Create(Current, [])); + KeyValue := TGocciaStringLiteralValue.Create(Key); + if Assigned(GC) then + GC.AddTempRoot(KeyValue); + try + EntryObj.DefineProperty(FOR_IN_ENTRY_KEY, + TGocciaPropertyDescriptorData.Create(KeyValue, [])); + finally + if Assigned(GC) then + GC.RemoveTempRoot(KeyValue); + end; + Result.Elements.Add(EntryObj); + finally + if Assigned(GC) then + GC.RemoveTempRoot(EntryObj); + end; + end; + Current := Current.Prototype; end; - Current := Current.Prototype; + finally + Visited.Free; + if Assigned(GC) then + GC.RemoveTempRoot(Obj); end; finally - Visited.Free; + if Assigned(GC) then + GC.RemoveTempRoot(Result); end; end; diff --git a/source/units/Goccia.Values.ObjectValue.pas b/source/units/Goccia.Values.ObjectValue.pas index 8ba8b6fd..329621b1 100644 --- a/source/units/Goccia.Values.ObjectValue.pas +++ b/source/units/Goccia.Values.ObjectValue.pas @@ -70,7 +70,6 @@ TGocciaObjectValue = class(TGocciaValue) function GetAllPropertyNames: TArray; virtual; function GetOwnPropertyNames: TArray; virtual; function GetOwnPropertyKeys: TArray; virtual; - function EnumerateForInPropertyNames: TArray; virtual; procedure DefineSymbolProperty(const ASymbol: TGocciaSymbolValue; const ADescriptor: TGocciaPropertyDescriptor); virtual; function TryDefineSymbolProperty(const ASymbol: TGocciaSymbolValue; const ADescriptor: TGocciaPropertyDescriptor): Boolean; virtual; @@ -1099,47 +1098,6 @@ function TGocciaObjectValue.GetAllPropertyNames: TArray; Result := FProperties.Keys; end; -// ES2026 §14.7.5.9 EnumerateObjectProperties(O) -function TGocciaObjectValue.EnumerateForInPropertyNames: TArray; -var - Current: TGocciaObjectValue; - Keys: TArray; - Descriptor: TGocciaPropertyDescriptor; - Names: TList; - Visited: TStringList; - Key: string; -begin - Visited := TStringList.Create; - Names := TList.Create; - try - Visited.CaseSensitive := True; - Current := Self; - while Assigned(Current) do - begin - Keys := Current.GetAllPropertyNames; - for Key in Keys do - begin - if Visited.IndexOf(Key) >= 0 then - Continue; - - Descriptor := Current.GetOwnPropertyDescriptor(Key); - if not Assigned(Descriptor) then - Continue; - - Visited.Add(Key); - if Descriptor.Enumerable then - Names.Add(Key); - end; - Current := Current.Prototype; - end; - - Result := Names.ToArray; - finally - Names.Free; - Visited.Free; - end; -end; - { Symbol property methods } // ES2026 §20.1.2.3.1 DefinePropertyOrThrow — symbol variant diff --git a/tests/language/for-in-loop/basic-enumeration.js b/tests/language/for-in-loop/basic-enumeration.js index 8646c1aa..7ffa7ceb 100644 --- a/tests/language/for-in-loop/basic-enumeration.js +++ b/tests/language/for-in-loop/basic-enumeration.js @@ -30,6 +30,24 @@ test("assigns to an existing member target", () => { expect(state.key).toBe("b"); }); +test("assigns to an existing private member target", () => { + class Collector { + #key = ""; + + collect() { + const keys = []; + for (this.#key in { a: 1, b: 2 }) { + keys.push(this.#key); + } + return [keys, this.#key]; + } + } + + const result = new Collector().collect(); + expect(result[0]).toEqual(["a", "b"]); + expect(result[1]).toBe("b"); +}); + test("skips non-enumerable own properties", () => { const obj = { visible: 1 }; Object.defineProperty(obj, "hidden", { diff --git a/tests/language/for-in-loop/traditional/for-in-mutates-counter.js b/tests/language/for-in-loop/traditional/for-in-mutates-counter.js new file mode 100644 index 00000000..a4b3117d --- /dev/null +++ b/tests/language/for-in-loop/traditional/for-in-mutates-counter.js @@ -0,0 +1,18 @@ +/*--- +description: for-in assignment targets disable counted-loop fast paths +features: [compat-for-in-loop, compat-traditional-for-loop] +---*/ + +test("for-in assignment target mutates the surrounding counter", () => { + const values = []; + const obj = { first: 1, second: 2 }; + + for (let i = 0; i < 3; i++) { + for (i in obj) { + break; + } + values.push(i); + } + + expect(values).toEqual(["first"]); +}); diff --git a/tests/language/for-in-loop/traditional/goccia.json b/tests/language/for-in-loop/traditional/goccia.json new file mode 100644 index 00000000..0c8a4b35 --- /dev/null +++ b/tests/language/for-in-loop/traditional/goccia.json @@ -0,0 +1,4 @@ +{ + "compat-for-in-loop": true, + "compat-traditional-for-loop": true +} diff --git a/tests/language/for-in-loop/var/var-shared-binding.js b/tests/language/for-in-loop/var/var-shared-binding.js index 96183147..93a72712 100644 --- a/tests/language/for-in-loop/var/var-shared-binding.js +++ b/tests/language/for-in-loop/var/var-shared-binding.js @@ -3,11 +3,36 @@ description: var in for-in heads hoists out of the loop features: [compat-for-in-loop, compat-var] ---*/ +for (var __gocciaForInEmptyGlobal in {}) {} +for (var [__gocciaForInEmptyDestructured] in {}) {} + test("var in for-in is visible after the loop", () => { for (var key in { a: 1, b: 2 }) {} expect(key).toBe("b"); }); +test("top-level var in empty for-in creates global property", () => { + const desc = Object.getOwnPropertyDescriptor( + globalThis, + "__gocciaForInEmptyGlobal" + ); + expect(typeof desc).toBe("object"); + expect(globalThis.__gocciaForInEmptyGlobal).toBeUndefined(); + + delete globalThis.__gocciaForInEmptyGlobal; +}); + +test("top-level var destructuring in empty for-in creates global property", () => { + const desc = Object.getOwnPropertyDescriptor( + globalThis, + "__gocciaForInEmptyDestructured" + ); + expect(typeof desc).toBe("object"); + expect(globalThis.__gocciaForInEmptyDestructured).toBeUndefined(); + + delete globalThis.__gocciaForInEmptyDestructured; +}); + test("var in for-in hoists into enclosing function", () => { const f = () => { for (var name in { first: 1 }) {} From aedf2b8168cdd739a110ca9695159e3a9f5bb5be Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Tue, 2 Jun 2026 22:54:14 +0200 Subject: [PATCH 3/3] Fix for-in probe traditional for fallback --- source/units/Goccia.Parser.pas | 7 +++++++ .../top-level-await/await-array-literal-header.js | 15 +++++++++++++++ .../language/for-loop/top-level-await/goccia.json | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 tests/language/for-loop/top-level-await/await-array-literal-header.js create mode 100644 tests/language/for-loop/top-level-await/goccia.json diff --git a/source/units/Goccia.Parser.pas b/source/units/Goccia.Parser.pas index 94311dfd..086a8e9e 100644 --- a/source/units/Goccia.Parser.pas +++ b/source/units/Goccia.Parser.pas @@ -4319,6 +4319,7 @@ function TGocciaParser.ForStatement: TGocciaStatement; BodyStmt: TGocciaStatement; TargetExpr: TGocciaExpression; SavedCurrent: Integer; + AfterLeftParen: Integer; Token: TGocciaToken; begin Line := Previous.Line; @@ -4343,6 +4344,7 @@ function TGocciaParser.ForStatement: TGocciaStatement; if Check(gttLeftParen) then begin Advance; // consume '(' + AfterLeftParen := FCurrent; // Check for const/let/var binding if Check(gttConst) or Check(gttLet) or (FVarDeclarationsEnabled and Check(gttVar)) then @@ -4463,6 +4465,11 @@ function TGocciaParser.ForStatement: TGocciaStatement; end else if not Check(gttSemicolon) then begin + FCurrent := SavedCurrent; + if FTraditionalForLoopsEnabled and LooksLikeTraditionalForHeader then + Exit(ParseTraditionalForBody(Line, Column)); + FCurrent := AfterLeftParen; + TargetExpr := Call; if Check(gttIn) then begin diff --git a/tests/language/for-loop/top-level-await/await-array-literal-header.js b/tests/language/for-loop/top-level-await/await-array-literal-header.js new file mode 100644 index 00000000..087f6338 --- /dev/null +++ b/tests/language/for-loop/top-level-await/await-array-literal-header.js @@ -0,0 +1,15 @@ +/*--- +description: top-level await array literals parse in traditional for headers +features: [compat-traditional-for-loop, top-level-await] +---*/ + +let count = 0; + +for (await []; await []; await []) { + count++; + break; +} + +test("top-level await array literals parse in traditional for headers", () => { + expect(count).toBe(1); +}); diff --git a/tests/language/for-loop/top-level-await/goccia.json b/tests/language/for-loop/top-level-await/goccia.json new file mode 100644 index 00000000..b04409b3 --- /dev/null +++ b/tests/language/for-loop/top-level-await/goccia.json @@ -0,0 +1,4 @@ +{ + "compat-traditional-for-loop": true, + "source-type": "module" +}