# oneecma docs, all pages (OneEcma 1.0.0) Every page of https://recursiveai.org/oneecma/docs/ and https://recursiveai.org/oneecma/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/oneecma/llms.txt # Getting started with OneEcma Install OneEcma for .NET 10, evaluate JavaScript from C#, pass values and functions in, read the error report when a script fails, run untrusted code under limits, and check source without running it. ## Install OneEcma targets `net10.0`, so you need the .NET 10 SDK. The packages share one version; this guide uses 1.0.0. ```shell dotnet new console -o OneEcmaStart cd OneEcmaStart dotnet add package RecursiveAI.OneEcma --version 1.0.0 ``` `RecursiveAI.OneEcma` is the whole engine, `Intl` included, with no dependency beyond the .NET base class library. It is NativeAOT- and trim-compatible. Two more packages are optional: - `RecursiveAI.OneEcma.Generators` writes host bindings for .NET types marked `[JsExport]` at build time. See [Expose .NET objects with [JsExport]](https://recursiveai.org/oneecma/cookbook/jsexport/). - `RecursiveAI.OneEcma.Cli` is the `oneecma` command: `dotnet tool install -g RecursiveAI.OneEcma.Cli`. See [The oneecma command](https://recursiveai.org/oneecma/cookbook/command-line/). The engine has no file, network, process or environment access of its own. A script reaches only what your code hands it. The snippets below are one program, in order. Put these `using` directives at the top of `Program.cs`: ```csharp using RecursiveAI.OneEcma; using RecursiveAI.OneEcma.Diagnostics; using RecursiveAI.OneEcma.Syntax; ``` ## Evaluate a script `ScriptEngine` is one engine with one global scope. `Evaluate` runs a script and returns its completion value as a `JsValue`: ```csharp var engine = new ScriptEngine(); JsValue result = engine.Evaluate("[1, 2, 3].map(n => n * 2).join('-')", "sum.js"); Console.WriteLine(engine.ToDisplayString(result)); Console.WriteLine(engine.Evaluate("6 * 7").AsNumber); ``` ```text 2-4-6 42 ``` The second argument is the file name reports and `err.stack` use. An engine isn’t thread-safe: use one per thread. ## Pass values and functions in Values go onto the global object through the engine’s realm. Put that setup in `ScriptEngineOptions.Configure`: when a script fails, the engine replays the run to explain it, and the replay calls `Configure` again to rebuild the same world. ```csharp var shop = new ScriptEngine(new ScriptEngineOptions { Configure = e => { JsRealm realm = e.MainRealm; JsObject order = realm.NewObject(); realm.DefineData(order, "id", JsValue.FromNumber(1042)); realm.DefineData(order, "items", JsValue.FromObject(realm.NewArray([JsValue.FromNumber(4), JsValue.FromNumber(6)]))); realm.DefineData(realm.Global, "order", JsValue.FromObject(order)); realm.DefineData(realm.Global, "tax", JsValue.FromObject( realm.NewFunction("tax", 1, (_, args) => JsValue.FromNumber(Math.Round(args[0].AsNumber * 0.08, 2)))), JsAttributes.Method); }, }); JsValue total = shop.Evaluate("const subtotal = order.items.reduce((a, b) => a + b, 0);\nsubtotal + tax(subtotal)", "total.js"); Console.WriteLine($"total: {total.AsNumber}"); ``` ```text total: 10.8 ``` A host function is `(this, args) => JsValue`. For whole .NET classes, records and enums, let the generator write the bindings: see [Expose .NET objects with [JsExport]](https://recursiveai.org/oneecma/cookbook/jsexport/). For tasks, exceptions in both directions and reading script objects back, see [Call C# from JavaScript](https://recursiveai.org/oneecma/cookbook/host-functions/). ## Read the report when a script fails A script error reaches C# as a `JsException`. `Message` is what the script would see as `` `${err.name}: ${err.message}` ``, and `Report` is the structured diagnostic; `DiagnosticRenderer.Render` prints it: ```csharp try { shop.Evaluate("const count = order.item.length;", "count.js"); } catch (JsException e) { Console.WriteLine(e.Message); Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); Console.WriteLine(shop.ToDisplayString(shop.MainRealm.Get(e.Value.AsObject!, "stack"))); } ``` ```text TypeError: Cannot read properties of undefined (reading 'length') error[TypeError]: Cannot read properties of undefined (reading 'length') --> count.js:1:21 | 1 | const count = order.item.length; | ^^^^ `order.item` is undefined | = help: did you mean `order.items`? (Array, 2 items) = stack: (count.js:1) TypeError: Cannot read properties of undefined (reading 'length') at count.js:1:26 ``` The headline and `err.stack` are V8’s: Node prints the same two lines for this script. The underline, the label and the suggestion come from a replay of the failed run with tracing on, so a run that succeeds pays nothing for them. The suggestion only ever names something the object actually holds. The same report comes as a compact rendering for a model’s context and as versioned JSON: see [Error reports: text, compact and JSON](https://recursiveai.org/oneecma/cookbook/error-reports/). ## Run untrusted code under limits `EngineLimits` bounds a run. When a limit is hit the run ends with a `JsLimitException`, which script can’t catch, and its report says which limit and where: ```csharp var sandbox = new ScriptEngine(new ScriptEngineOptions { Limits = new EngineLimits { Fuel = 1_000_000, WallClockMs = 2_000, MaxCallDepth = 2_000 }, }); try { sandbox.Evaluate("let n = 0;\nwhile (true) { n++; }", "spin.js"); } catch (JsLimitException e) { Console.WriteLine($"{e.Kind}: {e.Message}"); Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); } ``` ```text Fuel: Execution exceeded the step limit of 1000000 steps error[Limit]: Execution exceeded the step limit of 1000000 steps --> spin.js:2:1 | 2 | while (true) { n++; } | ^^^^^^^^^^^^^^^^^^^^^ | = stack: (spin.js:2) ``` Fuel is charged per call, per loop back-edge, per step of RegExp backtracking and per unit of work in long-running built-ins. The defaults are generous (4 billion steps, 2 GiB of allocation, no wall-clock limit), so set them for anything you don’t trust. Every limit, cancellation included, is in [Run untrusted JavaScript with limits](https://recursiveai.org/oneecma/cookbook/sandbox/). ## Check source without running it `ScriptEngine.Check` parses and runs the early errors only, and returns every error, not just the first: ```csharp foreach (Diagnostic d in ScriptEngine.Check("let a = 1;\nlet a = 2;\nlet b = ;", new ParseOptions { FileName = "two.js" })) { Console.WriteLine($"{d.Position.Line}:{d.Position.Column} {d.Code}: {d.Headline}"); } ``` ```text 2:5 redeclaration: Identifier 'a' has already been declared 3:9 unexpected-token: Unexpected token ';' ``` `Code` is a stable identifier that is never reworded. `ScriptEngine.CheckAndRepair` also applies the syntax-only repairs that have a single plausible fix: see [Run code a model wrote](https://recursiveai.org/oneecma/cookbook/run-model-code/). ## Next - [Run untrusted JavaScript with limits](https://recursiveai.org/oneecma/cookbook/sandbox/) - [Expose .NET objects with [JsExport]](https://recursiveai.org/oneecma/cookbook/jsexport/) - [Error reports: text, compact and JSON](https://recursiveai.org/oneecma/cookbook/error-reports/) - [The host API](https://recursiveai.org/oneecma/docs/host-api/), the reference for everything above - [What is supported](https://recursiveai.org/oneecma/docs/overview/#what-is-supported) and [how it is checked](https://recursiveai.org/oneecma/docs/overview/#conformance) # OneEcma **An interpreter for .NET implementing ECMA-262 / ECMAScript®, whose product is its error reports.** When a script fails, OneEcma says what went wrong, where the failing value came from and how to fix it, in a compact report that fits an LLM's context. `err.message` and `err.stack` stay exactly what V8 prints, so code that parses them keeps working. Pure C# on `net10.0` with no dependency beyond the .NET base class library; NativeAOT- and trim-compatible; every script is treated as hostile and runs under limits. OneEcma is not affiliated with or endorsed by Ecma International. ECMAScript® is a registered trademark of Ecma International. ## A failing script and its report `checkout.js`: ```js async function loadOrder(id) { return { id, lineItems: [{ price: 4 }, { price: 6 }], total: 10 }; } async function main() { const order = await loadOrder(7); const total = order.items.reduce((sum, i) => sum + i.price, 0); return total; } main(); ``` `oneecma run checkout.js` exits with status 1 and prints this on stderr: ``` error[TypeError]: Cannot read properties of undefined (reading 'reduce') --> checkout.js:7:23 | 6 | const order = await loadOrder(7); | ------------------ `order` comes from here: { id, lineItems, total } 7 | const total = order.items.reduce((sum, i) => sum + i.price, 0); | ^^^^^ `order.items` is undefined | = help: did you mean `order.lineItems`? (Array, 2 items) = stack: main (checkout.js:7) ``` The headline is V8's wording. The labelled spans, the origin of `order`, the suggestion (drawn only from what the object actually holds) and the stack come from replaying the run with tracing on, so a run that succeeds pays nothing for them. Syntax errors are reported all at once: ``` $ oneecma check two.js error[SyntaxError]: Identifier 'a' has already been declared --> two.js:2:5 | 1 | let a = 1; | ----- first declared here 2 | let a = 2; | ^^^^^ declared again here | = help: rename one of them, or assign with `a = 2` if `a` should change = note: not repaired automatically: every fix changes what the program means = note: a second error follows: both are reported from one parse error[SyntaxError]: Unexpected token ';' --> two.js:3:9 | 3 | let b = ; | ^ expected an expression before `;` | = help: give `b` a value, or remove the declaration error: aborting due to 2 syntax errors ``` The same reports are available as versioned JSON ([docs/diagnostic-json.md](https://recursiveai.org/oneecma/docs/diagnostic-json/)). ## Install ``` dotnet add package RecursiveAI.OneEcma # the engine, Intl included dotnet add package RecursiveAI.OneEcma.Generators # [JsExport] host bindings (analyzer only) dotnet tool install -g RecursiveAI.OneEcma.Cli # the `oneecma` command ``` All three packages are on nuget.org (1.0.0). `RecursiveAI.OneEcma.Generators` is an analyzer: it is referenced like any package but adds nothing to your output or dependencies. NativeAOT `oneecma` binaries for win-x64, linux-x64, linux-arm64 and osx-arm64, with sha256 files, are attached to each [GitHub release](https://github.com/recursive-ai-oss/oneecma-csharp/releases). To work on the library itself, build from source (below). ## Use the library Evaluate a script, under limits, and print the report when it throws: ```csharp using RecursiveAI.OneEcma; using RecursiveAI.OneEcma.Diagnostics; var engine = new ScriptEngine(new ScriptEngineOptions { Limits = new EngineLimits { Fuel = 50_000_000, WallClockMs = 2_000, MaxCallDepth = 2_000 }, }); try { JsValue result = engine.Evaluate("[1, 2, 3].map(n => n * 2).join('-')", "sum.js"); Console.WriteLine(engine.ToDisplayString(result)); // 2-4-6 } catch (JsException e) { Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); // e.Report is the structured report; e.Message is the short form } ``` Check source without running it (every syntax and early error, not just the first): ```csharp using RecursiveAI.OneEcma.Syntax; foreach (Diagnostic d in ScriptEngine.Check("let a = 1;\nlet a = 2;", new ParseOptions { FileName = "two.js" })) { Console.WriteLine(d.Headline); // Identifier 'a' has already been declared } ``` ### Host bindings with `[JsExport]` Mark .NET types and a source generator writes the marshalling at build time (no reflection at run time, so it works under NativeAOT). Only exported members exist in script. A mistake at the boundary names the .NET member, the argument path and the expected and actual types: ```csharp [JsExport] public record Item(string Sku, int Quantity, decimal Price); // copied by value: script passes plain objects [JsExport] public class Cart { public decimal Total(Item item) => item.Quantity * item.Price; } var host = new ScriptEngine(new ScriptEngineOptions { Configure = e => // also runs in the replay that explains a failure { JsExports.InstallAll(e.MainRealm); // generated e.MainRealm.DefineData(e.Global, "cart", e.MainRealm.Wrap(new Cart())); }, }); host.Evaluate("cart.total({ sku: 'pen', quantity: 3, price: 1.5 })", "a.js"); // 4.5 host.Evaluate("cart.total({ sku: 'pen', quantity: '3', price: 1.5 })", "bad.js"); ``` The second call throws a `JsException` whose report is: ``` error[TypeError]: Cart.Total: argument 'item.quantity' must be an integer (System.Int32), got string "3" --> bad.js:1:1 | 1 | cart.total({ sku: 'pen', quantity: '3', price: 1.5 }) | ^^^^^^^^^^ `item.quantity` is string "3", expected an integer (System.Int32) | = help: convert the string first: Number("3") = note: host function: Cart.Total(Item item) = stack: Cart.Total [.NET] ← (bad.js:1) ``` Exceptions cross in both directions: a .NET exception inside a host call reaches script as an `Error` that carries the .NET type, and a script throw reaching C# is a `JsException` with the full report; reports interleave script and .NET frames. [samples/OrderHost](https://github.com/recursive-ai-oss/oneecma-csharp/tree/aa60ea782eaf7093ce542d7bdc5570aef269d120/samples/OrderHost) is a larger worked example, and [docs/js-export.md](https://recursiveai.org/oneecma/docs/js-export/) lists what can be exported and the generator's diagnostics. ### Tools from a JSON Schema `JsonSchemaTools` registers a host function whose argument is checked against a JSON Schema before your handler runs, with the same error form. This is how an agent host exposes its tools to scripts: ```csharp JsObject fn = JsonSchemaTools.Define(realm, "createOrder", """{ "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } }""", args => args); realm.DefineData(realm.Global, "createOrder", JsValue.FromObject(fn), JsAttributes.Method); // createOrder({ id: 'x' }) -> TypeError: createOrder: argument 'args.id' must be an integer, got string "x" ``` ## The command line ``` oneecma run [--module] [--locals] [--compact] run a script; report on stderr and exit status 1 on an uncaught error; --locals adds the bindings of each frame (`= locals in f: a = 1, ...`); --compact prints the short report oneecma check [--module] [--compact] [--format text|json] [--source-map ] [--repair [--out ]] oneecma repl multi-line input, top-level await, .exit ``` `check` parses and runs the early errors only. Colour is used only on a terminal and never when `NO_COLOR` is set; `--format json` writes the JSON report to stdout. `--compact` (also `RenderOptions.Compact`) is for readers with limited context, such as language models: headline, `-->` location, the source line with its underline and label, the origin label when the value's source is known, the single most useful `help:`, and a one-line stack only when there is more than one frame. Blank gutter lines, `...` separators, extra helps and notes, locals and source-line context are dropped, and long lines are cut to about 100 columns. ## Two API layers - **Low level:** `JsValue` (a 16-byte struct, no boxing of numbers), `JsObject`, `JsRealm`, property and function callbacks, promises, modules, `IJsHost` for everything environmental (clock, randomness, locale, modules, output) and the job queue. Everything else is built from these public pieces. See [docs/host-api.md](https://recursiveai.org/oneecma/docs/host-api/). - **Happy path:** `ScriptEngine.Evaluate` and `ScriptEngine.Check`, `[JsExport]`, `JsonSchemaTools`, and the `oneecma` command. ## What is supported - The ES2026 language and built-ins (ECMA-262, 17th edition): strict and sloppy mode, modules and dynamic import, generators, async functions and async generators, classes with private members, BigInt, `Proxy` and `Reflect`, typed arrays, `Atomics` (single agent), `Date`, iterator helpers, and Annex B. Temporal is a later edition and is not included. - Our own backtracking RegExp engine with the `u` and `v` flags, lookbehind, named groups and Unicode properties; every backtracking step is charged to the fuel limit. - Source maps (ECMA-426): reports about minified or generated code point at the original source. - Intl (ECMA-402) for `en-US`, `en-GB`, `es` (including `es-US`), `fr`, `de` and `pt-BR`, Gregorian calendar and Latin digits, generated from CLDR 48.0 and Unicode 17.0.0. Unsupported locales fall back to the default locale. Adding a locale is a data change ([docs/locale-data.md](https://recursiveai.org/oneecma/docs/locale-data/)). ## Limits and sandbox A script reaches only what the host passes in: the engine has no file, network, process or environment access of its own. Every source of nondeterminism (clock, randomness, host-call results, job order) enters through `IJsHost` and is recorded, so a failed run is replayed exactly to build its report. `EngineLimits` bounds fuel (steps charged at calls, loop back-edges, RegExp backtracking and long-running built-ins), allocated memory, wall-clock time, call depth and string length. A limit ends the run with a report like any other error. Cancellation is a `CancellationToken` on every computing entry point. Parsing and the executor use explicit stacks or depth counters, so deep nesting in a script cannot overflow the .NET stack. Defaults are in the XML docs of `EngineLimits` and in [docs/runtime.md](https://recursiveai.org/oneecma/docs/runtime/). ## Auto-repair Off by default and syntax-only. `ScriptEngine.CheckAndRepair` (or `oneecma check --repair`) acts only on source that fails to parse, applies a fix only when it is the single plausible one and the result parses, and turns every fix that could change meaning into a suggestion. Applied repairs are listed first in the report and the repaired source is returned: ``` $ oneecma check --repair extra.js repair[R001]: removed an extra closing delimiter --> extra.js:2:38 | 2 | const doubled = items.map(x => x * 2)); | ^ removed | = note: the repaired source parses and is returned with this report const items = [1, 2, 3]; const doubled = items.map(x => x * 2); print(doubled); ``` Each rule, with examples, is in [docs/repair-table.md](https://recursiveai.org/oneecma/docs/repair-table/). ## Conformance Pass rates on the [test262](https://github.com/tc39/test262) suite pinned in `tools/test262/pin.txt`, run by `tools/OneEcma.Test262` in strict and sloppy variants. Every failing test is listed with its reason in `tools/test262/documented-engine.txt`; documented failures stay in the denominator. | Directory | Pass | Documented failures | Unexpected failures | |---|---:|---:|---:| | `language` | 22,973 | 31 | 0 | | `built-ins` | 18,337 | 146 | 0 | | `annexB` | 1,057 | 3 | 0 | | `intl402` (six locales) | 1,104 | 17 | 0 | Speed against Jint, including lodash, preact and a React server render checked byte for byte against Node, is in [docs/benchmarks.md](https://recursiveai.org/oneecma/docs/benchmarks/). `err.message` and `err.stack` are compared with Node on a differential corpus ([docs/stack-traces.md](https://recursiveai.org/oneecma/docs/stack-traces/)). Tests that need features of a later edition (Temporal and others) are skipped with a reason in `tools/test262/scope.txt`. ## Documentation [docs/README.md](https://recursiveai.org/oneecma/docs/docs-map/) indexes the behaviour documents: the host API, `[JsExport]`, the JSON report, record log, repair table, syntax-message wording, RegExp, Date, Intl and locale data. Every public type and member has an XML doc comment stating defaults, limits and what it throws. ## Build and test Requires the .NET 10 SDK. ```sh dotnet build OneEcma.slnx dotnet test --solution OneEcma.slnx dotnet pack src/RecursiveAI.OneEcma -c Release dotnet publish tests/OneEcma.AotSmoke -c Release -r linux-x64 # NativeAOT/trim check; use your RID ``` Build output goes to `artifacts/` (ignored). Set `ONEECMA_ARTIFACTS` to redirect it. | Path | Contents | |---|---| | `src/RecursiveAI.OneEcma` | the engine | | `src/RecursiveAI.OneEcma.Generators` | the `[JsExport]` source generator (analyzer-only package) | | `src/OneEcma.Cli` | the `oneecma` tool | | `tests/` | unit tests, generator tests, the NativeAOT smoke app | | `samples/` | a worked host example | | `fuzz/`, `bench/` | fuzz targets and the BenchmarkDotNet suite (Jint is referenced only in `bench/`, as a comparison target) | | `tools/` | the test262 runner, differentials against Node, data generators | | `docs/` | behaviour documents | ## Licence Licensed under either of [MIT](https://github.com/recursive-ai-oss/oneecma-csharp/tree/aa60ea782eaf7093ce542d7bdc5570aef269d120/LICENSE-MIT) or [Apache-2.0](https://github.com/recursive-ai-oss/oneecma-csharp/tree/aa60ea782eaf7093ce542d7bdc5570aef269d120/LICENSE-APACHE) at your option. Copyright Recursive AI, LLC. Third-party data and test suites are listed with their licences in [THIRD-PARTY-NOTICES.md](https://github.com/recursive-ai-oss/oneecma-csharp/blob/aa60ea782eaf7093ce542d7bdc5570aef269d120/THIRD-PARTY-NOTICES.md). # Host API (low level) The host API is the base layer: `[JsExport]` bindings, the JSON-Schema tool helper and `ScriptEngine.Evaluate` are all written with only the public types below, so a host can build its own happy path the same way. Nothing here uses reflection or emits code; it is safe under NativeAOT and trimming. All calls run on the thread that owns the `ScriptEngine` (an engine is not thread-safe). A host callback runs on that thread; a task returned by a host method resumes script on that thread when the engine is pumped (see "Tasks"). ## Types | Type | Role | |---|---| | `ScriptEngine` | One agent: realm, job queue, limits. `Evaluate`, `EvaluateAsync`, `EvaluateAsyncBody`, `CheckAsyncBody`, `CheckAndRepairAsyncBody`, `EvaluateModule`, `RunJobs`, `RunUntilIdle(Async)`, `Await(Async)`, `Call`, `GetProperty`, `SetProperty`, `DefineGlobalFunction`, `CreateRealm`, `MainRealm`, `Install`. | | `JsRealm` | A global object with its own built-ins. Creates and inspects values, defines properties, calls functions, makes promises, wraps .NET objects. `engine.MainRealm`, `engine.CreateRealm()`. | | `JsValue` | 16-byte struct: undefined, null, boolean, number, string, symbol, BigInt, object. `FromNumber/FromString/FromBoolean/FromBigInt/FromObject/FromSymbol`, `IsX` tests, `AsNumber/AsString/AsBoolean/AsBigInt/AsSymbol/AsObject`. | | `JsObject` | Any object, opaque; use `JsRealm` to read and write it. `JsHostObject` is the object that stands for a .NET object (`Target`). | | `JsAttributes` | `Writable`, `Enumerable`, `Configurable`, `All`, `Method` (writable and configurable, hidden: the default for host members). | | `JsPromise` | A promise made in C#: `Value`, `State`, `Result`, `Resolve`, `Reject`, `OnSettled`. | | `HostFunction` / `HostConstructor` | Delegates for host functions `(this, args) -> value` and constructors `(args, newTarget) -> value`. | | `JsException` | A thrown script value. `new JsException(value)` throws a script value from a host function; `Report` is the `Diagnostic`; `InnerException` is the original .NET exception when the error came from one. | | `JsLimitException` | A limit (fuel, memory, time, depth) ended the run. | | `IJsHost` | Clock, randomness, locale, modules, printing: everything environmental. `DefaultHost` is the system implementation; `InMemoryModuleHost` serves modules from strings. | ## Values, properties and functions ```csharp var engine = new ScriptEngine(); JsRealm realm = engine.MainRealm; JsObject cart = realm.NewObject(); realm.DefineData(cart, "total", JsValue.FromNumber(12.5)); // enumerable, writable, configurable realm.DefineData(cart, "id", JsValue.FromString("c1"), JsAttributes.Enumerable); // read-only realm.DefineAccessor(cart, "double", realm.NewFunction("get double", 0, (_, _) => JsValue.FromNumber(realm.ToNumber(realm.Get(cart, "total")) * 2)), setter: null, JsAttributes.Enumerable); realm.DefineData(realm.Global, "cart", JsValue.FromObject(cart)); JsObject items = realm.NewArray([JsValue.FromString("pen"), JsValue.FromNumber(3)]); foreach (JsValue key in realm.OwnKeys(cart)) // spec order: integer keys, strings, then symbols if asked Console.WriteLine(realm.ToDisplayString(key)); JsObject math = realm.Get(realm.Global, "Math").AsObject!; JsValue max = realm.Call(realm.Get(math, "max"), JsValue.Undefined, JsValue.FromNumber(1), JsValue.FromNumber(9)); ``` - `NewFunction(name, length, fn)` makes a function; a .NET exception thrown inside it reaches script as an `Error` (below). - `NewConstructor(name, length, construct, out prototype, basePrototype)` makes a class: calling without `new` is a TypeError, a script `class X extends Host` works, and `PrototypeFor(newTarget, fallback)` gives subclasses their prototype. - `NewSymbol(description)`, `WellKnownSymbol("iterator")`, `NewError("RangeError", message)` create the values they say. - `Get`, `Set`, `Has`, `Delete`, `DefineData`, `DefineAccessor`, `OwnKeys`, `GetPrototypeOf`, `Call`, `Construct` run the spec's operations, so getters, setters and proxy traps run and may throw `JsException`. - `Describe(value)` gives the short text used in boundary errors (`number 5`, `an array of 3 items`, `an object with keys id, total`). ## Realms `engine.CreateRealm()` returns a second realm with its own `Object`, `Array`, … and global. `realm.Evaluate(source, name)` runs a script in it. Values move between realms freely; each function runs in the realm it was created in. Limits and the job queue are the engine's. ## Promises, jobs, tasks ```csharp JsPromise p = realm.NewPromise(); realm.DefineData(realm.Global, "ready", p.Value); engine.Evaluate("ready.then(v => print(v))"); p.Resolve(JsValue.FromString("go")); engine.RunJobs(); // runs the reaction JsValue v = engine.Await(engine.Evaluate("fetchThing()")); // pumps jobs until settled; throws JsException on rejection JsValue w = await engine.EvaluateAsync("(async () => 1 + 1)()"); // also waits for host tasks without blocking a thread ``` - `Evaluate`, `RunJobs`, `Call` drain the job queue. `RunUntilIdle` additionally waits for host tasks (below) to finish and runs what they resume; `RunUntilIdleAsync` does so without blocking the thread. - `Await(value)` pumps until the promise settles and returns its value, or throws `JsException` with the reason. A promise that can never settle (nothing queued, no host task running) is an error, not a hang. - A task returned by a host function becomes a promise (`JsMarshal.FromTask`). While the host method runs, a synchronization context routes its `await` continuations back to the engine's thread, so the callbacks it makes into script are safe; they run when the engine is pumped. The host that calls `Evaluate` (not `EvaluateAsync`) must pump with `RunUntilIdle`/`Await`. - Unhandled rejections end a run with the rejection's `JsException` unless `ScriptEngineOptions.ThrowOnUnhandledRejection` is false. - A host task may be complete when it reaches the engine (`Task.FromResult`, a cached task, an `async` method with a synchronous fast path), faulted or cancelled, or throw before it returns a task. All of them become a promise that is settled (fulfilled or rejected with the .NET exception as a script `Error`) before the call returns, and all of them record and replay exactly like a task that finishes later. - When the engine explains a failure it replays the recorded run. If that is not possible the plain report stays and gains a note, `= note: explanation unavailable: `: the run cannot be replayed (with the reason from `RecordLog.NotReplayableReason`), the replay diverged from the recorded run (with what differed), the replay did not fail the same way, or the rejected promise was not the result of an entry call. The error and its headline are never replaced. ## Running code as an async function body Hosts that run code written by someone else (an LLM, a user) usually want `await` and `return` at the top level, a fresh scope per call, and positions that point into the text they were given. `ScriptEngine.EvaluateAsyncBody` does that without a wrapper: ```csharp JsValue p = engine.EvaluateAsyncBody("const o = await api.getOrder(id);\nreturn o.items.length;", "checkout.js"); JsValue n = await engine.AwaitAsync(p); // throws JsException with the explained report if the body failed ``` - **Goal.** The text is parsed with the goal of an `AsyncFunctionBody` (`ParseGoal.AsyncFunctionBody`), the one `new AsyncFunction(body)` uses (ECMA-262 15.8), as the body of an anonymous `async` function with no parameters. `await` and `return` are valid at the top level; `yield`, `super` and `import`/`export` are not; `new.target` is allowed and is undefined. Sloppy unless the text starts with `"use strict"`. - **Scope.** Every `let`, `const`, `var`, function and class declaration belongs to the call and disappears with it, so two calls never collide and the global object gains nothing (an assignment to an undeclared name still creates a global in sloppy code, as anywhere). Free names resolve against the global object and the host's globals; `this` is the global object, in strict bodies too. - **Positions.** The source is not wrapped or shifted: line and column in reports, `err.stack` and `Diagnostic` spans are those of the text passed in, so no source map is needed. The body's frame reads `` in reports and has no function name in `err.stack` (`at checkout.js:2:10`); a frame stitched across an `await` reads `async `. - **Result.** Returns the promise of the body's completion: fulfilled with the `return` value (undefined when none), rejected with a thrown error. The call drains the job queue first, so a body that never waits on a host task has already settled. A body waiting on a host task returns a pending promise; wait with `Await`/`AwaitAsync`, or pump with `RunUntilIdle(Async)`. With the default `ThrowOnUnhandledRejection`, a body that rejected without anyone handling it makes `EvaluateAsyncBody` (or the pump that ran the failing continuation) throw `JsException`; a host that awaits the promise it was given gets the same exception from `Await`. - **Syntax errors.** `EvaluateAsyncBody` throws `JsException` (a `SyntaxError` report) for the first error. `ScriptEngine.CheckAsyncBody(source, fileName)` returns every syntax and early error without running, and `CheckAndRepairAsyncBody` applies the syntax-only repair table with the same goal and returns the repaired source to pass to `EvaluateAsyncBody`. - **Record and replay.** The call is an entry call of the log (`AsyncBody`), and `Await`/`AwaitAsync` on its promise is logged, so a failure is explained like any other: provenance of the failing value, did-you-mean, locals and the async stack, across any host task that was awaited. ## Modules Modules come from `IJsHost.ResolveModule(specifier, referrerId, attributes)`. `engine.EvaluateModule("main.js")` resolves the entry, loads its imports through the same call and returns the namespace object. `InMemoryModuleHost` resolves `./` and `../` specifiers against the importer's slash-separated id: ```csharp var host = new InMemoryModuleHost().Add("lib/util.js", "export const twice = x => x * 2;") .Add("main.js", "import { twice } from './lib/util.js'; export const answer = twice(21);"); var engine = new ScriptEngine(new ScriptEngineOptions { Host = host }); JsValue ns = engine.EvaluateModule("main.js"); ``` ## .NET objects in script `JsRealm.Install(JsHostType)` creates a class for an exported type (the generator's `XJsBinding.JsType`; `JsExports.InstallAll(realm)` installs every exported type of the assembly). `realm.Wrap(obj)` returns the script object for a .NET object: the same object each time, with the members the type exports on its prototype. Only exported members exist in script; `Wrap` of an object whose class was not installed throws `InvalidOperationException` that says what to install. `NewHostObject(target, prototype)` is the primitive underneath. ## Errors across the boundary | Direction | What happens | |---|---| | .NET exception in a host function | Script sees `Error` with message `: ` and a non-enumerable `dotnetType` (the full type name). It is catchable. If it reaches C# uncaught it is `JsException` whose `InnerException` is the original and whose `Report` lists the .NET frames. | | Script throw reaching C# | `JsException` with `Value`, `Report`, `Frames`. | | Argument conversion failure | `TypeError`, message `: argument '' must be , got ` (see `js-export.md`). | | A `JsException` thrown by a host function | Passes through unchanged (`throw new JsException(realm.NewError("RangeError", "…"))`). | Reports interleave frames: `StackFrameInfo.IsHost` marks a .NET frame (`Function` is the member, `HostLine` its line when the runtime knows it). Text shows `Member (File.cs:38)` or `Member [.NET]` when there is no line (NativeAOT keeps method names only). JSON adds `host: true` and `hostLine`. Frames from the engine, the generated bindings and the BCL are left out. ## Limits Script that calls a host function spends fuel for the call like any other call; a loop that calls .NET is bounded by the step limit. `NewObject(prototype)`, `NewArray` and host-object creation are charged to the memory limit. A script callback invoked from .NET runs under the same limits and can end the run with `JsLimitException`. The time and cost of the .NET code itself is the host's to bound. ## What changed in other types `JsValue` gained `AsBoolean`, `AsBigInt`, `AsSymbol`, and `AsString`/`FromSymbol` became public. `JsException` has a public constructor. `StackFrameInfo` gained `IsHost` and `HostLine`. `ScriptEngine` is `partial`; its new members live in `Hosting/`. # `[JsExport]`: .NET types as script classes Mark a type with `[JsExport]` and a Roslyn generator (the `RecursiveAI.OneEcma.Generators` package, analyzer-only) writes its binding at build time. There is no reflection at run time, so it works under NativeAOT and trimming. Only exported members exist in script. ```csharp [JsExport] public enum OrderStatus { Pending, Paid, Shipped } [JsExport] public record LineItem(string Sku, int Quantity, decimal Price); [JsExport] public record Order(int Id, List LineItems, OrderStatus Status = OrderStatus.Pending, string? Note = null); [JsExport] public class OrderApi { public Order GetOrder(int id) { … } public Task FetchOrderAsync(int id) { … } public void ForEach(Action visit) { … } } // host JsExports.InstallAll(engine.MainRealm); // generated, in the project's root namespace engine.MainRealm.DefineData(engine.Global, "api", engine.MainRealm.Wrap(new OrderApi())); ``` ```js const order = await api.fetchOrderAsync(2); // a Promise api.forEach(o => print(o.id)); // a callback api.addOrder({ id: 3, lineItems: [{ sku: "pen", quantity: 1, price: 1.5 }] }); ``` A runnable version is `samples/OrderHost`. Reference the generator as an analyzer (`PackageReference` with `PrivateAssets="all"` for the package); it reads the project's `RootNamespace` through `CompilerVisibleProperty`, which the package's `build/*.props` sets. ## What gets exported | Declaration | In script | |---|---| | `class` (default) | A class: `new C(...)` for each public constructor, instance members on its prototype, statics on the constructor. Instances are **shared by reference**: script holds a handle to the .NET object. Derived exported classes inherit through the prototype chain. | | `interface` | A prototype for values returned through it; no constructor. `Wrap(obj)` finds the prototype of the object's class first. | | `static class` | A plain global object holding the static members. | | `record`, `struct` | **Copied by value**: a script object becomes a new .NET value; a .NET value becomes a new plain object. Only data members (public readable properties and fields) take part. Override with `Kind = JsExportKind.Reference`/`Value`. | | `enum` | A global object of member names; values cross as the member **name** (case-insensitive on the way in) or its number. | Members of an exported class: public instance and static methods, properties (get/set accessors on the prototype; a read-only property has no setter, so assignment is ignored in sloppy code and a TypeError in strict code), fields (as accessors), `const` fields (read-only values). Members of `object` (`Equals`, `GetHashCode`, `GetType`, an unannotated `ToString`) are never exported. `[JsIgnore]` leaves a member out; `[JsExport(Name = "x")]` renames it. **Names.** Members become camelCase (`GetOrder` → `getOrder`, `URL` → `url`, `ID` → `id`; a leading run of capitals lowers all but the last when a lowercase follows: `HTMLPage` → `htmlPage`). Types keep their name. Enum member names are kept as written. **Overloads.** Overloads share one script name. A call picks the most specific overload whose parameter kinds accept the arguments: integer, number, string, boolean, bigint, array, plain object, function, host object of a given class, enum (string or number), `JsValue` (anything). `int` is chosen over `double` for integral numbers. Overloads that accept the same kinds for some argument count are a build error (OE1004); a call that matches none is a TypeError listing the overloads. **Parameters.** Required, optional (`= default`, or a trailing nullable parameter), and `params T[]`. A missing or `undefined` optional argument takes the default. **Supported types.** `bool`, all integer types (`long`/`ulong` cross as a number when exact, else a BigInt; a BigInt is accepted when it fits), `float`, `double`, `decimal` (finite numbers), `System.Numerics.BigInteger` (BigInt), `string`, `char` (one-character string), enums, `T?` and nullable reference types (`null` and `undefined` give null), `T[]`, `List` and the usual interfaces (`IList`, `IReadOnlyList`, `IEnumerable`, `ICollection`) as arrays, `Dictionary` and its interfaces as plain objects, `JsValue` and `JsObject` (passed through), `Func<…>`/`Action<…>` up to six parameters (script functions; a callback that returns a `Task`/`Task` awaits the promise the script function returns), and other exported types. **Returns.** `void` (undefined), values as above, `Task`, `Task`, `ValueTask`, `ValueTask` (a Promise), `IEnumerable` (an iterator: items are produced as script asks, `return()` disposes), `IAsyncEnumerable` (an async iterator whose `next()` returns promises). An exception thrown synchronously by a task-returning method rejects the promise instead of throwing. **Conversions are strict.** A number is not accepted where a string is expected, and a string is not accepted where a number is: the failure says so and suggests the conversion (`Number("7.25")`). By-value types read their properties by exported name; a property missing from the script object is an error when the .NET member is required (a constructor parameter without default, a `required` member, a non-nullable property) and leaves the default otherwise. `RejectUnknownProperties = true` makes an extra property an error that suggests the closest name. ## Boundary errors Every error has the form `: …` as a `TypeError` whose `Diagnostic` has the call as the primary span, a label stating the path and the finding, `help:` lines, `= note: host function: ` and a stack whose first frame is the .NET member: ``` error[TypeError]: Shop.OrderApi.AddOrder: argument 'order.lineItems[1].price' must be a finite number (System.Decimal), got string "7.25" --> case.js:6:1 | 6 | api.addOrder({ id: 3, lineItems: lines }); | ^^^^^^^^^^^^ `order.lineItems[1].price` is string "7.25", expected a finite number (System.Decimal) | = help: convert the string first: Number("7.25") = note: host function: Shop.OrderApi.AddOrder(Order order) = stack: Shop.OrderApi.AddOrder [.NET] ← (case.js:6) ``` | Situation | Headline after `: ` | |---|---| | wrong type | `argument '' must be , got ` | | missing argument or property | `argument '' is missing; expected ` | | unknown property (when rejected) | `argument '' has no property ''` + `help: did you mean ''?` + the accepted names | | enum name | `argument '' must be one of 'A', 'B' (Enum), got string "…"` + did-you-mean | | callback result | `the value returned by callback '' must be , got ` | | no overload | `no overload accepts ()` + one note per overload | | wrong `this` | `called on an incompatible receiver (this is ); expected a object` | The path is the parameter name followed by `.property` and `[index]` steps. `` is `undefined`, `null`, `number 1.5`, `string "…"` (cut at 40 characters), `boolean true`, `bigint 5n`, `a function`, `an array of 3 items`, `an object with keys a, b`, `an empty object` or `a symbol`. These messages are ours (V8 has no counterpart); the name of the error and its place in `err.stack` follow V8. Nesting of by-value types is limited to 64 levels in both directions, so a script object that contains itself or a cyclic .NET graph ends in an error (`TypeError` naming the path, or `RangeError` copying to script) instead of overflowing the stack. A .NET exception thrown by an exported member arrives as `Error: : ` with `dotnetType`, and as `JsException.InnerException` in C#. Its report lists the .NET frames (with file and line when debug information exists, method names only under NativeAOT) before the script frames. ## Build-time diagnostics All are errors with a fix in the message. | Id | Raised when | Fix | |---|---|---| | OE1001 | the declaration cannot be exported: generic type, `ref struct`, delegate, type nested in a non-accessible type | export a closed or non-generic type | | OE1002 | a member shape has no script form: `ref`/`out`/`in` parameter, indexer, event, generic method, ref return | `[JsIgnore]`, or change the shape (events: take an `Action<…>`) | | OE1003 | a member uses a type that cannot cross: `object`, an unexported class, `Dictionary`, multi-dimensional array, pointer, a task as a parameter | add `[JsExport]` to the type, use `JsValue`, use `Dictionary` | | OE1004 | two overloads accept the same kinds of script values for some argument count | rename one with `[JsExport(Name = "…")]`, make kinds differ, or `[JsIgnore]` | | OE1005 | two members (or two exported types) get the same script name | rename or ignore one | | OE1006 | a by-value type cannot be built from a script object | add a parameterless constructor, or one whose parameters are named like its properties | | OE1007 | a `Name` is not an identifier | letters, digits, `_`, `$`, not starting with a digit | | OE1008 | `Kind` does not fit the type (a by-reference struct; a by-value interface, static or abstract class) | change `Kind` or the type | A type with errors produces no binding and is left out of `JsExports.InstallAll`; the other types still generate. ## Generated code For each exported type `T` the generator writes `TJsBinding` (same namespace; `internal` when the type is) with `JsType` (a `JsHostType` for `realm.Install`), and `JsExports.g.cs` with `InstallAll(JsRealm)` and `Names` in the project's root namespace. By-value types also get `TryFromJs`/`ToJs`. The code is plain C# over the public types in `host-api.md`; read it by building with `EmitCompilerGeneratedFiles=true`. Output order is deterministic. `tests/OneEcma.Generators.Tests/Snapshots` holds the expected text for five shapes. ## JSON-Schema tools `JsonSchemaTools` registers a function whose single argument is checked against a JSON Schema before the handler runs, with the same error form (`createOrder: argument 'args.items[0].price' must be a number, got string "2"`, path root `args`): ```csharp JsObject fn = JsonSchemaTools.Define(realm, "createOrder", schemaJson, arg => /* JsonElement in, JsonElement out */); JsObject typed = JsonSchemaTools.Define(realm, "search", schemaJson, ToolJson.Default.SearchArgs, ToolJson.Default.SearchHitArray, args => Search(args)); JsObject slow = JsonSchemaTools.DefineAsync(realm, "slow", schemaJson, async arg => …); // returns a Promise JsObject checkedCall = JsonSchemaTools.Validated(realm, "name", schemaJson, existingFunction, thisValue); // wrap an exported method realm.DefineData(realm.Global, "createOrder", JsValue.FromObject(fn), JsAttributes.Method); ``` In 1.0.0 the `JsonElement` a `DefineAsync` handler receives is valid only until the handler first awaits: read what you need from it, or keep `arg.Clone()`, before the first `await`. Typed overloads take `JsonTypeInfo` from a `System.Text.Json` source-generated context, so they are AOT-safe; the context's naming policy decides the .NET property names (use camel case to match schemas). The script value is converted to JSON by own enumerable properties (functions, symbols and `undefined` properties are left out; non-finite numbers become `null`; nesting deeper than 64 is an error). Supported keywords: `type` (also a list), `enum`, `const`, `properties`, `required`, `additionalProperties` (false or a schema), `items`, `minItems`, `maxItems`, `minLength`, `maxLength` (UTF-16 units), `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `pattern` (.NET regular expression with a 200 ms limit; a timeout counts as no match), `allOf`, `anyOf`, `oneOf`, `not`, local `$ref`. Everything else is ignored. A schema that is not JSON is an `ArgumentException`. # Runtime guide: adding a built-in area This is the contract for code under `Builtins/`. You can write a built-in area (a constructor, its prototype and its static functions) from this page and the spec alone, without reading the VM. Everything here is `internal` to `RecursiveAI.OneEcma` unless marked public. **Frozen** means: do not change the shape of it. Additions that old code ignores are fine. A change that breaks a caller needs the maintainers of the core to agree first. | Frozen | Not frozen | |---|---| | `JsValue` (public constructors `FromX`, predicates, `AsNumber`/`AsObject`/`AsString`), `PropertyKey`, `PropertyDescriptor`, `PropFlags`, the virtual internal methods of `JsObject`, `NativeFn`, `Realm.Fn/Method/Getter/Value/Ctor`, `Errors.Type/Range/Reference/Syntax/Make`, `Agent.Tick/Charge/Alloc/EnqueueJob`, `IJsHost`, `EngineLimits` | `Shape` internals, `Bytecode`, `Vm`, `Frame`, `Generators`, `Modules`, the `Realm` named intrinsic fields (new ones may be added) | ## Values `JsValue` is a 16-byte readonly struct: an object reference plus a double. Numbers never allocate. The reference says which kind it is. | Kind | Make | Test | Read | |---|---|---|---| | undefined | `default`, `JsValue.Undefined` | `IsUndefined` | | | null | `JsValue.Null` | `IsNull`, `IsNullish` (undefined or null) | | | boolean | `JsValue.True/False`, `FromBoolean` | `IsBoolean` | `Ops.ToBoolean(v)` | | number | `FromNumber(double)` | `IsNumber` | `AsNumber` | | string | `FromString(string)` | `IsString` | `AsString` (flattens a rope) | | symbol | `FromSymbol(JsSymbol)` (internal) | `IsSymbol` | `(JsSymbol)v.Ref` | | bigint | `FromBigInt(BigInteger)` | `IsBigInt` | `(BigInteger)v.Ref` | | object | `FromObject(JsObject)` | `IsObject` | `AsObject`, `v.Ref as JsObject` | `Tdz` and `Hole` are internal sentinels for uninitialised bindings and array holes; they never reach a built-in as an argument. A string may be a rope internally; always use `AsString`. Equality is SameValue (`==` on `JsValue`); use `Ops.SameValueZero`, `Ops.StrictEquals`, `Ops.LooseEquals` for the other spec comparisons. Inside `Ops` (static, in `Runtime/Ops.cs`) you find the abstract operations by their spec names: `ToPrimitive`, `ToNumber`, `ToNumeric`, `ToIntegerOrInfinity`, `ToInt32`, `ToUint32`, `ToLength`, `ToIndex`, `ToStr` (ToString), `ToObject`, `ToPropertyKey`, `RequireObjectCoercible`, `IsCallable`, `IsConstructor`, `IsArray`, `LengthOfArrayLike`, `Call`, `Invoke`, `Construct`, `GetMethod`, `GetV`, `SetV`, `OrdinaryHasInstance`, `InstanceOf`, `SpeciesConstructor`, `OrdinaryCreateFromConstructor`, `GetPrototypeFromConstructor`, `ListFromArrayLike`, `CreateArray`, `CreateIterResult`, `GetIterator`, `IteratorStepValue`, `IteratorClose`, `IterableToList`. Each performs the observable steps in spec order, so calling it is the correct way to do "Let x be ? ToString(y)". `Ops.Arg(args, i)` is the argument or undefined. ## Properties and objects `PropertyKey` is a string or a `JsSymbol`. A `string` converts implicitly (`obj.Get("length")`). Array indices are ordinary string keys in the API; `key.TryGetIndex(out uint)` recognises canonical indices. `PropFlags`: `Writable`, `Enumerable`, `Configurable`, `Accessor`, with the combinations `None` (all false), `Default` (W+E+C, what `CreateDataProperty` makes), `Hidden` (W+C, not enumerable: the usual flags for built-in methods), `Configurable` alone (what `length` and `name` have). `PropertyDescriptor` has `HasValue/Value`, `HasWritable/Writable`, `HasGet/Get`, `HasSet/Set`, `HasEnumerable/Enumerable`, `HasConfigurable/Configurable`, `IsAccessor`, and `PropertyDescriptor.Data(value, flags)`. `JsObject` implements the ordinary internal methods of ECMA-262 10.1 as virtuals. Exotic objects override exactly the ones the spec says they override, and nothing else: | Spec method | C# | Notes | |---|---|---| | [[GetPrototypeOf]] / [[SetPrototypeOf]] | `GetPrototypeOf()` / `SetPrototypeOf(p)` | returns bool | | [[IsExtensible]] / [[PreventExtensions]] | `IsExtensibleOp()` / `PreventExtensions()` | | | [[GetOwnProperty]] | `GetOwnProperty(key, out desc)` | returns whether it exists | | [[DefineOwnProperty]] | `DefineOwnProperty(key, in desc)` | returns bool; `ValidateAndApply` is ValidateAndApplyPropertyDescriptor | | [[HasProperty]] | `HasProperty(key)` | | | [[Get]] / [[Set]] | `Get(key, receiver)`, `Get(key)`; `Set(key, value, receiver)` | | | [[Delete]] | `Delete(key)` | | | [[OwnPropertyKeys]] | `OwnKeys(List)` | appends in spec order (indices ascending, strings in creation order, symbols) | | [[Call]] / [[Construct]] | `Call(this, args)`, `Construct(args, newTarget)` | overridden by function objects | Helpers on `JsObject` that are the spec's abstract operations: `CreateDataProperty`, `CreateDataPropertyOrThrow`, `DefinePropertyOrThrow`, `HasOwnProperty`, `SetOrThrow(key, v)`. For building a built-in's own properties use the *direct* definers, which skip the checks because the object is fresh: `DefineDirect(key, value, flags)` and `DefineAccessorDirect(key, getter, setter, flags)`. To make an exotic object, subclass `JsObject` (or `JsArray` for array-likes that store elements densely), pass the prototype to the base constructor, override the internal methods your spec section lists, and set `ClassName`. Internal slots are plain C# fields on your subclass: an `IsMyThing(this)` check is `thisValue.Ref is MyThing`. Do not use shape slots for internal state. Large objects are cheap to build one property at a time: a `Shape` indexes its keys only after it has been looked up several times, so a built-in prototype with dozens of `DefineDirect` calls does not index every intermediate shape. Code that holds a `Frame` or an `Env` past the call that created it must mark it (`Frame.Escaped`, `Env.MarkCaptured`), because finished frames and uncaptured environments are reused; see `executor.md`. ### Deferred own properties An ordinary function's own `prototype` object (ECMA-262 10.2.5 MakeConstructor) is created the first time anything can observe it, because most closures never use it. `ScriptFunction` created with `lazyPrototype: true` has the shape from `Realm.FunctionShape(proto, 3)`: `length`, `name`, then `prototype` flagged as an accessor with an empty slot, and `JsObject.Lazy` is set. The flag marks the slot as not a data property, so no cached or direct slot read ever uses it. The function overrides `GetOwnProperty` and `TryGetOwn` (for the key `prototype`) and `DefineOwnProperty`, `Delete`, `OwnKeys`, `SetPrototypeOf` and `PreventExtensions` (always) to call `Realize()`, which creates the object, switches to the ordinary shape (`FunctionShape(proto, 1)`) and clears `Lazy`. The non-virtual storage primitives on `JsObject` (`AddProperty`, `ReplaceProperty`, `DefineDirect`, `DefineAccessorDirect`, `RemoveSlot`) realize first, so a property added to a function still comes after `prototype`. A new virtual that exposes or changes an object's own keys must do the same if it can be reached on a `Lazy` object; code that only reads `length` and `name` through the shape needs nothing. `Shape.DigitKeys` counts the keys that start with a decimal digit (kept in step by `Add` and `Remove`, also in dictionary mode). `ArrayAlgo.ChainIndexFree` uses it, so `push` and similar fast paths decide in constant time per prototype whether an index key could be intercepted. ### Strings built by concatenation `a + b` of two strings whose joined length is 24 or more makes a `Rope` node and charges 48 bytes. The characters are charged when the rope is first observed (`AsString`, `Rope.Flatten`), which copies once from the end of the result, following the left spine without a stack and keeping a small stack only for right children that are themselves ropes; flattening never recurses on the host stack. A flattened node keeps its string and drops its children. `length` on a string primitive and a `String.prototype` method lookup (`GetStringProp` in the VM) do not flatten. A value that a host function receives is always a flat `string`. While a limit report is being described, memory charges are recorded but do not raise a second limit (describing a frame previews its string locals, which flattens ropes). ## Native functions ```csharp internal delegate JsValue NativeFn(JsValue thisValue, ReadOnlySpan args, JsObject? newTarget); ``` `newTarget` is null for a call and the constructor for `new`/`Reflect.construct`. A function that is not a constructor ignores it. Arguments beyond `args.Length` are undefined: always read with `Ops.Arg(args, i)`. Do not keep `args` beyond the call (it is a span over the VM stack). `Realm` registers things. Inside `Builtins.SetupXxx(Realm r)`: ```csharp JsObject proto = new JsObject(r.ObjectPrototype); // the prototype object NativeFunction ctor = r.Ctor("Thing", 1, (t, a, nt) => { // defines global Thing, %Thing%, .prototype, .constructor if (nt is null) throw Errors.Type("Constructor Thing requires 'new'"); JsObject o = Ops.OrdinaryCreateFromConstructor(nt, static x => x.ObjectPrototype); // honours newTarget.prototype return JsValue.FromObject(o); }, proto); r.Method(proto, "size", 0, (t, a, nt) => ...); // writable, configurable, non-enumerable; name and length set r.Method(proto, WellKnown.Iterator, "[Symbol.iterator]", 0, fn); r.Getter(proto, "size", "size", (t, a, nt) => ...); // accessor, name "get size" Realm.Value(ctor, "MAX", JsValue.FromNumber(7), PropFlags.None); r.Intrinsics["%ThingPrototype%"] = proto; // for other areas and for realm-aware lookup ``` `r.Fn(name, length, fn)` makes an anonymous-parent function value without defining it anywhere. Register the area by adding a `SetupThing(r)` call to `Builtins.Setup` (order matters only when your area needs another's prototype: call yours after it). Use `r.Intrinsics` for anything another area or the VM must find by name. Add a named field on `Realm` only when the VM needs it on a hot path. `Agent.Current` is the running engine on this thread; `Ops.CurrentRealm` is its current realm (the realm of the running native function). Built-ins that run when the realm differs from the creating realm (cross-realm `newTarget`) use `Ops.GetPrototypeFromConstructor`, which does the spec's GetFunctionRealm fallback. ## Throwing Throw `Errors.Type(message)`, `Errors.Range`, `Errors.Reference`, `Errors.Syntax`, or `Errors.Make("URIError", message)`. They return a `JsException` to `throw`. They create the Error object in the current realm and capture the stack. The `message` is the headline text exactly as Node prints it in `err.message`: take it from running Node on the failing call, never invent it. Use `Ops.DisplayValue(v)` where V8 interpolates a value (`Object prototype may only be an Object or null: undefined`) and `Ops.ObjectLabel(o)` for `#`. ```csharp if (thisValue.Ref is not JsThing self) throw Errors.Type("Method Thing.prototype.size called on incompatible receiver " + Ops.DisplayValue(thisValue)); ``` Never throw another .NET exception type on purpose; an unexpected one becomes an internal-error report. Limits are thrown by the agent (below), not by you. ## Fuel, memory and time `Agent.Current.Tick()` spends one unit of fuel; `Charge(n)` spends n. At the end of each 4096-unit chunk the agent polls the cancellation token and the wall clock, and throws `JsLimitException` when fuel, time or cancellation says stop. Script cannot catch it. Rule: **every native loop whose trip count depends on script data calls `Tick()` once per iteration** (or `Charge(n)` with n the unit count for a bulk operation), including loops that only call into non-charging code. That covers `sort`, `join`, `repeat`, `padStart`, `flat`, `fill`, `indexOf`, `copyWithin`, `splice`, string building, iteration helpers, and RegExp backtracking. Calls into script (`Ops.Call`) are charged by the VM already. A loop of a few constant iterations needs nothing. Before allocating something sized by script data (a string result, an array of length n, a buffer), call `Agent.Current.Alloc(bytes)` first. It counts bytes *requested* cumulatively (not live bytes) against `EngineLimits.MaxAllocatedBytes`. Strings longer than `EngineLimits.MaxStringLength` must throw `Errors.Range("Invalid string length")`. Arrays of length over 2^32-1 throw `RangeError: Invalid array length`. ## No host recursion that follows script depth The VM runs JS-to-JS calls on an explicit frame stack, so JS recursion does not consume the .NET stack. Native code that calls back into script (`Ops.Call`, `Construct`, a getter reached by `Get`, a Proxy trap) does consume it; the VM counts these re-entries and ends the run past `EngineLimits.MaxNativeDepth` (default 120). So: - Calling script from a native is fine; the count is automatic. - Native code that recurses on its own structure when that structure is script-controlled (deep arrays in `flat`, nested objects in `JSON.stringify`/`parse`, the proto chain in `OrdinaryHasInstance`, Proxy chains) must use an explicit stack or carry a depth counter and throw `Errors.Range("Maximum call stack size exceeded")` at a fixed bound (e.g. 1000 for data recursion). Prefer iteration. - Never `lock`, block, or call `.Result` on anything; the engine is single-threaded per agent and re-entrancy is the model. ## The host `IJsHost` (public, frozen) is the only way out. A built-in that needs the time, randomness, the locale, the zone, a module or output asks `Agent.Current.Host`: `CurrentTimeMs()` (Date), `MonotonicMs()` (limits, `performance.now`), `NextRandom()` (`Math.random`), `DefaultLocale`, `TimeZoneId`, `ResolveModule`, `Print`, `JobEnqueued`, `PromiseRejectionTracker`, `InitializeImportMeta`, `WantsWeakCollection`. Never call `DateTime.Now`, `Stopwatch`, `Random` or the environment yourself; `HostDefaults.cs` is the one file that may. Anything new that is nondeterministic becomes a new `IJsHost` member with a default in `DefaultHost` (additive, so existing hosts keep compiling) and a line here. Promise jobs go through `Agent.EnqueueJob(Action)`; never run a reaction synchronously. `WeakRef` and `FinalizationRegistry` observations are scheduled by the engine through the job queue, never by GC timing. ### Wording that names an object or a value Every row below was recorded from Node v24 (`testdata/errors/cases.txt`, ids `iter-*` and `fnstr-*`) and is compared with the engine on each test run. - **Object label** (`Ops.ObjectLabel`), used by `Cannot delete property 'k' of L`, `Cannot set property k of L which has only a getter` and, inside quotes, `Cannot assign to read only property 'k' of object 'L'`: a function is its `Function.prototype.toString` text (and the message says `function '...'` instead of `object '...'`); an Error is `Name: message`; an array, String/Number/Boolean/Symbol/BigInt wrapper, Date, RegExp or typed array is `[object Class]`; anything else is `#` where C is the name of the constructor function found by reading `constructor` along the prototype chain without running script (a script function's own declared name wins over a redefined `name`), or `[object Object]` when there is none (null prototype, `constructor` not a function). A Proxy is `#`. - **Store to a string primitive** (strict): `length` and indices below the length give `Cannot assign to read only property 'k' of string 's'`; any other key gives `Cannot create property 'k' on string 's'`. - **`Object.defineProperty` on a non-extensible object** adding a new key: `Cannot define property k, object is not extensible`; any other refusal: `Cannot redefine property: k`. - **Value kind** (`Ops.KindText`): `undefined`, `object null`, `symbol`, `bigint`, `function`, `object`, `string "s"`, `number 5`, `boolean true`. Used by `X is not iterable (cannot read property Symbol(Symbol.iterator))` from built-ins, `yield*`, array-pattern parameters and assignment patterns, and by `X is not a function` when `GetMethod` finds a non-callable (`Symbol.toPrimitive`, `Symbol.hasInstance`, `Symbol.asyncIterator`). - **Source text** in `X is not iterable` for `for-of`, spread and declaration initializers: an identifier, member chain, `null`, `true`/`false`, a number, a regular expression literal, `{}` (`{(intermediate value)}` with properties), `this`. A declaration whose initializer is any other expression, and every nested or assignment pattern, uses the value kind instead. A nullish spread argument in a call reads `X is not iterable (cannot read property undefined)` (`null` for null); a non-iterable non-nullish one reads `Spread syntax requires ...iterable[Symbol.iterator] to be a function`. - **`Array.from`** with a `Symbol.iterator` that is neither nullish nor callable: `%Array%.from requires that the property of the first argument, items[Symbol.iterator], when exists, be a function`. - **`Function.prototype.toString`**: script functions give their source; native functions `function name() { [native code] }` with the name they were created with (a later `name` redefinition is not shown; getters read `get size`, symbol-named ones `[Symbol.replace]`); bound functions, Proxies with a callable target and the revoker give `function () { [native code] }`; a Proxy with a non-callable target throws TypeError. The `at` frame of a call on a Proxy receiver names the type `Proxy`. ### Weak-reference collection points Every `WeakRef` and `FinalizationRegistry` target is held strongly until a collection point; only there do unreachable targets die. Two things create one: - A script calling `$262.gc()` (the conformance host). - The host, through `IJsHost.WantsWeakCollection()`. The engine asks it once per job-queue drain (`Agent.RunJobs`, which ends every `Evaluate`, `EvaluateModule` and `RunJobs` entry call), after the last job and the idle hook, and only if a weak target has ever been created. On true it runs a collection point, then drains any cleanup jobs that queued. The interface default is false, so a custom host that does nothing keeps the old behaviour. `DefaultHost` (so the CLI too) answers true once `GC.GetTotalMemory(false)` has risen by `WeakCollectionGrowthBytes` (protected virtual, default 8 MiB) above the lowest level seen at an earlier question; after a yes it measures again from the next level. A long run that creates weak targets therefore releases them as memory grows and holds them while it does not allocate. The figure is the host's, so it lives in `HostDefaults.cs`. Replay: the question is not logged. A yes is followed by the usual `Weak` event (written by the collection point itself, with the dead ids), and nothing but the tag of the next entry call follows a drain, so a replaying host answers yes exactly when the next event is `Weak`. The log format is unchanged (version 1). `EngineLimits` (public, frozen): `Fuel` (default 4e9 steps), `MaxAllocatedBytes` (2 GiB), `WallClockMs` (none), `MaxCallDepth` (10,000, catchable RangeError), `MaxNativeDepth` (120, uncatchable), `MaxStringLength`. A new limit is a new property with a default. ## Tests for a built-in area Scripts run through `ScriptEngine.Evaluate`. Write a handful of xUnit tests under `tests/OneEcma.Tests/` for behaviour that could plausibly break, and run the matching test262 directory through `tools/OneEcma.Test262` (see `tools/test262/`). Failures you do not fix go into `tools/test262/documented-engine.txt` under your area name, with the reason. # Diagnostic JSON report (version 1) The JSON form of a report. It carries everything the text report shows, as data, so a host, an editor or an agent tool can read it without parsing text. The schema is `docs/diagnostic-report.schema.json` (JSON Schema 2020-12, `$id` `urn:oneecma:diagnostic-report:1`). The writer is `DiagnosticJson`; the CLI prints it with `oneecma check --format json ` (on standard output; the text form goes to standard error). ## Rules - **Versioned.** The top-level `"version"` is `1`. Within a version, fields are only ever added. A consumer ignores properties it does not know. A change that removes, renames or re-types a field, or changes what a value means, is a new version. - **Offsets and positions.** `start` and `end` are offsets in UTF-16 code units into the source text, half-open (`end` is one past the last code unit). `line` and `column` are 1-based; **columns count UTF-16 code units** (a character outside the Basic Multilingual Plane is two columns), the same unit V8 uses in `err.stack`. `endLine` and `endColumn` are the position of `end`. A line ends at LF, CR, CRLF, U+2028 or U+2029. An empty span has `start == end`. - **Strings.** Output is UTF-8. A lone UTF-16 surrogate in the script source (legal in JavaScript, not representable in JSON) is written as U+FFFD. Offsets still count the original code units. - **Absent versus empty.** Core arrays (`secondary`, `notes`, `help`, `fixes`, `recovery`) are always present and may be empty. Optional members (the ones marked *optional* below) are left out when there is nothing to say, never written as `null` or an empty array. - **Deterministic.** The same diagnostics give the same bytes. Property order is as listed here. Compact by default (one line); `JsonRenderOptions.Indented` gives two-space indentation (used by the corpus files). ## Document ```json { "version": 1, "format": "oneecma.report", "summary": { "errors": 1, "warnings": 0, "repairs": 0 }, "repairs": [ ... ], // optional: applied repairs, first to last "repairedSource": "...", // optional: present exactly when "repairs" is "diagnostics": [ ... ] } ``` `summary.errors` and `summary.warnings` count the diagnostics by severity; `summary.repairs` counts the applied repairs. Repairs are listed before diagnostics because a repaired run's report starts with what was changed. ### Diagnostic | field | type | meaning | |---|---|---| | `kind` | string | error constructor name (`SyntaxError`, ...), or `SourceMap` for a problem with a source map | | `code` | string | stable machine-readable id such as `unexpected-token`; never reworded once published | | `severity` | `"error"` \| `"warning"` | | | `headline` | string | word for word what V8 puts in `err.message` for the same mistake (for `SourceMap` diagnostics, our own wording) | | `source` | `{ name, length }` | the source every span below refers to; `length` in code units | | `span` | span | the primary span; the `-->` line of the text report is its start | | `label` | string | the text beside the primary underline; may be empty | | `secondary` | `[{ span, message }]` | secondary labelled spans (underlined with `-` in text) | | `notes` | string[] | the `= note:` lines | | `help` | string[] | the `= help:` lines | | `fixes` | `[{ description, edits }]` | suggested changes; none is applied. An edit is `{ start, length, line, column, text }`: the range `[start, start+length)` becomes `text` | | `recovery` | `[{ action, span, text }]` | what the parser assumed (`insertedToken`, `text` is the token) or skipped (`skippedTokens`) to continue | | `mapping` | object, *optional* | present when a source map was applied, see below | A span is `{ start, end, line, column, endLine, endColumn }`. ### Repair `{ id, title, label, source, span, edits }`: the rule id from the repair table (`R001`), what it did, the underline text, the original source, the first place it touched and every edit it made (offsets into the original source). Applying all repairs' edits in the order listed to the original source gives `repairedSource`. ### `mapping` (optional) Added by `SourceMapReports` when a mapping exists for the primary span: ```json "mapping": { "spansAreOriginal": true, "original": { "source": "src/app.ts", "line": 5, "column": 42, "endLine": 5, "endColumn": 43, "name": "greet" }, "generated": { "source": "dist/app.js", "line": 5, "column": 34, "endLine": 5, "endColumn": 35 }, "generatedSpan": { "start": 107, "end": 108 } } ``` - `spansAreOriginal: true`: the original text was available, so `source` and every span of the diagnostic are in the **original** source; `generated` and `generatedSpan` say where the problem is in the generated code. Suggested fixes and recovery steps (offsets into the generated text) are dropped. - `spansAreOriginal: false`: the map has an entry but no text for the original source; `source` and spans still refer to the generated code, and `original` says where the problem is in the original file (the text report adds an `original position:` note). - `name` (optional) is the identifier the map names at that position. ### Runtime explanation members (optional) A runtime report built by a traced replay (the default for an uncaught error when recording is on) writes these members; each is present only if non-empty. Shapes are in the schema. Version 1 stays: they were defined as optional slots from the start and nothing changed shape. | field | content | |---|---| | `stack` | frames, innermost first: `{ function, source, span, async, locals: [{ name, value }] }`. `locals` lists the bindings of the frame's function scope (script top level: the script's `let`/`const`/`class` bindings), innermost scope first, at most 24 per frame, each value a preview of at most 60 characters (`{ id, lineItems, total }` for an object: its first 6 own enumerable keys; `Array (3 items)`; literals for primitives). Only present on a traced report. | | `provenance` | where the failing value came from, nearest step first: `{ source, span, description }`. Each step is the span of an expression a binding or property was written from (`const order = await api.getOrder(id)`), followed by the earlier places the same object was stored; the chain is capped at 8 steps and a note says so when it is cut. The same spans are the report's secondary labels. | | `didYouMean` | candidates drawn from what the object or scope actually holds, best first: `{ name, description }`. A candidate is a key of the object, a binding visible at the failing place, or a key whose value has the member being called (`order.lineItems`, `result.items.map`); ranking is case-insensitive equality, then substring either way, then optimal-string-alignment distance up to max(2, length/3). Never a name the object or scope does not hold. | | `asyncStack` | reserved, not written: frames stitched across `await` are in `stack` with `async: true` | ## Files `testdata/golden//expected.json` is the report for each `syn-*` case as `DiagnosticJson.Render(diagnostics, Indented)` plus a newline, and for `map-001-*` the remapped report. A test reads each back and checks its headlines, spans and counts against `expected.txt` and against an independent line/column count. # `err.stack` and the frames of a report `err.stack` follows V8's format and the engine matches Node's output on the differential corpus (`testdata/errors/cases.txt`, 213 snippets; `expected.json` is what Node v24.22.1 printed, recorded by `tools/errors-diff/record.js`). The report's `= stack:` line and the JSON `stack` array come from the same captured frames, so they never disagree with `err.stack`. ## Format ``` : at at ``` - The header is `Name: message` (just `Name` when the message is empty). It is formatted when `stack` is first read, so a `name` or `message` set after construction shows, as in V8. - At most `Error.stackTraceLimit` frames (default 10) are kept, innermost first, captured when the error object is created. - A frame line is one of: - `at fn (file:line:col)` for a named function; - `at Type.method (file:line:col)` for a method call whose receiver has a class, where `Type` is the name of the constructor the receiver inherits (`String`, `Number`, `Boolean`, `Symbol`, `BigInt`, `Function`, a class name, `Object` for a plain object unless it has a `Symbol.toStringTag`); - `at new Fn (file:line:col)` for a construct call; - `at file:line:col` for top-level code and anonymous functions; - `at async fn (file:line:col)` for an async function that is suspended at an `await` behind the running frames; - `at Array.map ()` for a built-in function between script frames; - `at eval (eval at f (file:line:col), :line:col)` for code made by `eval` or `new Function`. - The error constructors, `Array` and `Proxy` are left out of the trace of the error they create; frames of a subclass constructor are skipped up to the `new.target` of the construction. - Columns count UTF-16 code units, 1-based. ## Positions V8 reports a position inside the expression, not the start of the statement: | Construct | Position | |---|---| | property read or method call `a.b` / `a.b()` | the property name `b` | | call `f()` | the start of the callee `f` | | call of another expression `(x)()`, `a()()` | the operator that follows the callee, skipping `)` and comments | | binary operation, assignment, compound assignment | the operator | | `new C()` | the `new` keyword | | tagged template | the template | | `yield` | the start of its operand | | destructuring a value that cannot be unpacked | the first property key of the pattern | Each bytecode instruction carries one such position (`FunctionCode.Pos`); the frame's span (used for the report's underline) is the span of the failing expression and is kept separately. ## Frames of a report - Built-in frames (`Array.map`) appear in `err.stack` but not in the report; the report points at the script code that called them. - A repeated frame (same function, source and line, consecutive) collapses to `fn (file:line) ×N` in the report line; `err.stack` and the JSON keep every frame. - Top-level code is `` in the report line and has `IsTopLevel` set; an anonymous function is ``. - Async frames: when an async function is suspended at an `await` of a promise made by another async function, the awaiting function is stitched behind the bottom frame (`async main`). Only direct awaits are stitched. - Proper tail calls are off by default so the caller stays in the trace (as in V8); `ScriptEngineOptions.ProperTailCalls` turns them on (the test262 runner does). ## Differences from Node (documented, `testdata/errors/documented.txt`) 1. The frame at the call-depth limit differs: V8's limit depends on native stack left, ours is a fixed depth (10,000). 2. A derived constructor that returns without calling `super()`: V8 points at the parameter list, the engine at the body. 3. Class field initializers run inline (frame named after the field); V8 shows a synthetic `` frame. 4. `new Function` with a parameter list that closes early: V8's message is "Arg string terminates parameters early". 5. `await Promise.all(...)`: V8 stitches an `async Promise.all (index n)` frame; the engine does not. 6. A `for await` loop is reported at its binding by V8, at the `for` statement by the engine. 7. Assigning to `undefined` in strict code names the receiver `[object Object]` in V8's vm context, `#` in the engine. # Help lines for runtime errors The `= help:` line of a runtime report names the fix. It reads live values from a traced replay and never runs script. ## "Is not a function" on a missing method When `recv.name(...)` fails because the receiver has no such member, the help is chosen in this order: 1. The receiver is a Promise: `` `x` is a Promise; `await` it first``. 2. **Idiom table** (below): a name other languages or libraries use that JavaScript's built-ins do not have. The help gives the JavaScript form. 3. A primitive of the other kind has the method (`Number(s).toFixed`). 4. A property holding the thing being called (`result.items.map`). 5. Edit distance: `did you mean recv.name2? (function)`, from members the receiver really has. A spelling near-miss never outranks the idiom table, and the edit-distance rule is strict for short names: a name of up to four characters may differ by one edit only (`sum` no longer suggests `some`, `size` no longer suggests `slice`); longer names allow `max(2, length / 3)` edits. A name that contains another counts as close only when the shorter has five or more characters or is at least two thirds of the longer (`items` in `lineItems`, but not `sum` in `summary`). Names that differ only by case, `_` or `-` (`createdAt`, `created_at`) are the closest match of all. ### Idiom table | Receiver | Name | Help | |---|---|---| | Array | `sum`, `total` | `a.reduce((a, b) => a + b, 0)` | | Array | `max`, `min` | `Math.max(...a)`, `Math.min(...a)` | | Array | `average`, `avg`, `mean` | `a.reduce((a, b) => a + b, 0) / a.length` | | Array | `contains`, `has` | `a.includes(value)` | | Array | `size`, `len` | the property `a.length` | | Array | `count` | `a.length`, or `a.filter(item => ...).length` | | Array | `first`, `last` | `a[0]`, `a.at(-1)` | | Array | `isEmpty`, `empty` | `a.length === 0` | | Array | `append`, `add` | `a.push(value)` | | Array | `remove`, `delete` | `a.splice(index, 1)`, or `a.filter(...)` for a copy | | Array, String | `length()` called | `length` is a property: `a.length` | | Map, Set | `size()` called | `size` is a property | | String | `contains` | `s.includes(text)` | | String | `len`, `size` | the property `s.length` | | String | `strip`, `lstrip`, `rstrip` | `trim()`, `trimStart()`, `trimEnd()` | | String | `toUpper`, `upper`, `toLower`, `lower` | `toUpperCase()`, `toLowerCase()` | | String | `format` | a template literal | | plain Object | `keys`, `values`, `entries` called as methods | `Object.keys(o)` and so on | | plain Object | `items` | `Object.entries(o)` | | plain Object | `has`, `hasKey`, `contains` | `key in o`, `Object.hasOwn(o, key)` | | Map, Set | `map`, `filter`, `reduce`, `some`, `every`, `find` | `[...m].map(...)` | | Map, Set | `contains` | `m.has(key)` | | Map, Set | `length`, `count`, `len` | the property `m.size` | The entries apply only when the receiver does not have a member of that name (the `length()` and `size()` rows are the reverse: the member exists and is a number). An own property of the same name always wins. ## Invalid Date When `Invalid time value` is thrown on a Date made by `new Date(x)` in the traced run, and `x` was an `undefined` read from a property, the report labels the call (`` `new Date(e.createdAt)`: `e.createdAt` is undefined, so the Date is invalid``) and gives the missing-property help for the object that was read (`did you mean e.created_at?`, with camelCase and snake_case counted as the same word). Otherwise the help is the guard `if (Number.isNaN(d.getTime()))`. ## NaN made from an undefined In the traced run, arithmetic, unary `+`/`-`, `++`/`--` and calls to built-ins (`Number(x)`, `Math.round(x)`) that are handed an undefined from a property read, a call or a `find` produce a NaN that remembers that undefined (the id of its origin record travels in the NaN's payload bits; nothing outside a traced replay sees it, and every NaN test treats it as NaN). Where a built-in rejects such a NaN (`new Array(n)`: `Invalid array length`; `BigInt(n)`: `The number NaN cannot be converted to a BigInt because it is not an integer`), the primary label says `` `x.count * 2` is NaN, computed from an undefined`` and the missing-property label and did-you-mean for the read follow, as for any other undefined. A Date made from such a NaN is explained like a Date made from the undefined itself. A NaN that no undefined produced (`0 / 0`) gets no origin, and nothing is reported for a NaN that never reaches an error. ## Iterating the result of a call `for (const x of obj.items())`, `[...obj.items()]` and `const [a] = f()` fail with V8's one wording for both causes: `obj.items is not a function or its return value is not iterable`. The report looks at the callee: when it is not a function it is explained as a failed call (did you mean, what it is), and when it is a function the result is blamed (`` `obj.items(...)` returned something that is not iterable``) with the help to return an array or to loop over `Object.keys`/`values`/`entries`. Which operand shapes use this wording follows what Node prints (`iter-*` rows of `testdata/errors/cases.txt`): the callee text of a call or `new` in `for-of` and array spread (a comma expression is spelled `(0 , f)`, calls in callee position lose their `(...)`), `for await` (`obj.o(...) is not a function or its return value is not async iterable`), a declaration initializer whose callee is a plain name (`const [a] = f()`; a member callee gives the value-kind wording), and `yield*` of anything but a name (`yield* (intermediate value) is not iterable`). A spread argument in a call keeps `Spread syntax requires ...iterable[Symbol.iterator] to be a function`. # Syntax error wording The headline of every syntax error and early error is word for word what V8 puts in `err.message`, so scripts and error-reporting libraries that match on it keep working. The parser never invents a headline: explanations go in the label, `help` and `note` lines of the report. ## How the wording was learned By running Node as a black box. Each snippet in `testdata/syntax-messages/snippets.json` is compiled with `new vm.Script` (`tools/node/record-syntax-messages.js`) and the message and line of the thrown `SyntaxError` are recorded in `cases.json`. Node v22.22.1 and v24.22.1 print the same text for all 176 snippets. The test `SyntaxMessageTests` checks that our first error has the same headline and line, and that snippets Node accepts produce no error. ## Rules the wording follows - A token the grammar cannot accept at that point: `Unexpected token 'X'` for punctuators and keywords, `Unexpected identifier 'name'`, `Unexpected number`, `Unexpected string`, `Unexpected template string`, `Unexpected end of input`, `Unexpected reserved word` (`enum`, and `await` where it is reserved), `Unexpected strict mode reserved word`. - A malformed token (bad number, escape, character, unterminated comment): `Invalid or unexpected token`; the more specific literal messages in the table below apply to numeric separators, escapes and octal forms. - `@` is not a token in ES2026 (decorators are a later edition), so it is `Invalid or unexpected token`. - A call missing its closing parenthesis: `missing ) after argument list`, positioned at the previous token. - An unterminated template literal reports `Unexpected end of input`. - `await x` outside an async function or module reports V8's `await is only valid in async functions and the top level bodies of modules`, positioned at `await`. - Regular expression pattern errors are `Invalid regular expression: /pattern/flags: reason`. The pattern body is validated by the RegExp engine through `ParseOptions.RegExpValidator`; the parser itself only reports a missing closing `/` and invalid flags. - The position of an error is the first token V8 could not accept, with these exceptions that follow V8: end-of-input errors point at the last line that has content; `try` without `catch` points at the closing brace of the block. ## Table One row per distinct headline, with a snippet that produces it (all verified against Node). | Headline | Example | |---|---| | `'arguments' is not allowed in class field initializer or static initialization block` | `class C { static { arguments } }` | | `'super' keyword unexpected here` | `class A extends B { m(){ super() } }` | | `A class may only have one constructor` | `class A { constructor(){} constructor(){} }` | | `Async functions can only be declared at the top level or inside a block.` | `a: async function f(){}` | | `Cannot use 'import.meta' outside a module` | `import.meta` | | `Cannot use import statement outside a module` | `import x from 'y'` | | `Class constructor may not be an accessor` | `class A { get constructor(){} }` | | `Classes may not have a static property named 'prototype'` | `class A { static prototype(){} }` | | `Decimals with leading zeros are not allowed in strict mode.` | `'use strict'; 08` | | `Delete of an unqualified identifier in strict mode.` | `'use strict'; delete a` | | `Duplicate __proto__ fields are not allowed in object literals` | `({__proto__:1,__proto__:2})` | | `Duplicate parameter name not allowed in this context` | `function f(a,a){'use strict'}` | | `Function statements require a function name` | `function(){}` | | `Generators can only be declared at the top level or inside a block.` | `a: function* g(){}` | | `Getter must not have any formal parameters.` | `({get a(x){}})` | | `Identifier '#a' has already been declared` | `class A { #a; #a }` | | `Identifier 'a' has already been declared` | `function f(){ let a; var a }` | | `Identifier 'e' has already been declared` | `try {} catch(e){ let e }` | | `Identifier 'x' has already been declared` | `let x; var x` | | `Illegal 'use strict' directive in function with non-simple parameter list` | `function f(a=1){'use strict'}` | | `Illegal await-expression in formal parameters of async function` | `async function f(a = await 1){}` | | `Illegal break statement` | `break` | | `Illegal continue statement: no surrounding iteration statement` | `continue` | | `Illegal return statement` | `return` | | `In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.` | `while(1) function f(){}` | | `In strict mode code, functions can only be declared at top level or inside a block.` | `'use strict'; if(1) function f(){}` | | `Invalid Unicode escape sequence` | `'\u12'` | | `Invalid destructuring assignment target` | `({a:1} = 1)` | | `Invalid hexadecimal escape sequence` | `'\x1'` | | `Invalid left-hand side expression in postfix operation` | `1++` | | `Invalid left-hand side expression in prefix operation` | `++a++` | | `Invalid left-hand side in assignment` | `a?.b = 1` | | `Invalid left-hand side in for-loop` | `for (a, b of c);` | | `Invalid optional chain from new expression` | `new a?.b` | | `Invalid or unexpected token` | `3in []` | | `Invalid regular expression flags` | `/abc/gg` | | `Invalid regular expression: /(/: Unterminated group` | `/(/` | | `Invalid regular expression: /(?<=a)+/: Invalid quantifier` | `/(?<=a)+/` | | `Invalid regular expression: /(?.)(?.)/: Duplicate capture group name` | `/(?.)(?.)/` | | `Invalid regular expression: /[b-a]/: Range out of order in character class` | `/[b-a]/` | | `Invalid regular expression: /\2(a)/u: Invalid escape` | `/\2(a)/u` | | `Invalid regular expression: /\k(?.)/: Invalid named capture referenced` | `/\k(?.)/` | | `Invalid regular expression: /\p{Foo}/u: Invalid property name` | `/\p{Foo}/u` | | `Invalid regular expression: /a**/: Nothing to repeat` | `/a**/` | | `Invalid regular expression: /{1}/u: Nothing to repeat` | `/{1}/u` | | `Invalid regular expression: missing /` | `/abc` | | `Invalid shorthand property initializer` | `({a=1})` | | `Invalid tagged template on optional chain` | `a?.`x`` | | `Keyword must not contain escaped characters` | `var \u0069f` | | `Label 'label' has already been declared` | `label: label: 1` | | `Lexical declaration cannot appear in a single-statement context` | `if (1) let x` | | `Missing initializer in const declaration` | `const a;` | | `Missing initializer in destructuring declaration` | `var [a];` | | `Numeric separator can not be used after leading 0.` | `0_1` | | `Numeric separators are not allowed at the end of numeric literals` | `1_` | | `Octal escape sequences are not allowed in strict mode.` | `'use strict'; '\01'` | | `Octal escape sequences are not allowed in template strings.` | ``\01`` | | `Octal literals are not allowed in strict mode.` | `'use strict'; 010` | | `Only one underscore is allowed as numeric separator` | `1__0` | | `Private field '#a' must be declared in an enclosing class` | `this.#a` | | `Private field '#b' must be declared in an enclosing class` | `class A { m(){ this.#b } }` | | `Rest element must be last element` | `[...a,]=1` | | `Rest parameter may not have a default initializer` | `function f(...a=1){}` | | `Rest parameter must be last formal parameter` | `(...a,b)=>1` | | `Setter function argument must not be a rest parameter` | `({set a(...x){}})` | | `Setter must have exactly one formal parameter.` | `({set a(){}})` | | `Strict mode code may not include a with statement` | `'use strict'; with(a){}` | | `The left-hand side of a for-of loop may not be 'async'.` | `for (async of x);` | | `Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence` | `-1 ** 2` | | `Undefined Unicode code-point` | `'\u{110000}'` | | `Undefined label 'x'` | `break x` | | `Unexpected end of input` | ``abc` | | `Unexpected eval or arguments in strict mode` | `'use strict'; var eval` | | `Unexpected identifier 'a'` | `({ async get a(){} })` | | `Unexpected identifier 'b'` | `[a b]` | | `Unexpected identifier 'bar'` | `foo bar` | | `Unexpected identifier 'x'` | `for (let of x);` | | `Unexpected identifier 'yield'` | `function* g(){ function yield(){} }` | | `Unexpected number` | `1n.5` | | `Unexpected reserved word` | `var enum` | | `Unexpected strict mode reserved word` | `'use strict'; var let` | | `Unexpected string` | `'a' 'b'` | | `Unexpected token '('` | `(a,b)=>{} ()` | | `Unexpected token ')'` | `)` | | `Unexpected token '*'` | `function* g(){ yield\n* 1 }` | | `Unexpected token ','` | `x = {a:1,,}` | | `Unexpected token '=>'` | `a\n=> 1` | | `Unexpected token '??'` | `a \|\| b ?? c` | | `Unexpected token 'class'` | `if (1) class C {}` | | `Unexpected token 'export'` | `export var a` | | `Unexpected token '\|\|'` | `a ?? b \|\| c` | | `Unexpected token '}'` | `}` | | `Yield expression not allowed in formal parameter` | `function* g(a = yield){}` | | `\8 and \9 are not allowed in strict mode.` | `'use strict'; '\8'` | | `await is only valid in async functions and the top level bodies of modules` | `await 1` | | `for-in loop variable declaration may not have an initializer.` | `'use strict'; for (var x = 1 in y);` | | `for-of loop variable declaration may not have an initializer.` | `for (let x = 1 of y);` | | `let is disallowed as a lexically bound name` | `let let = 1` | | `missing ) after argument list` | `f(a b)` | | `new.target expression is not allowed here` | `new.target` | # Auto-repair table (v2) Changes since v1: R012 added (rank 7; every later rank moves down one), R001 also removes a run of adjacent unmatched closers, R007 also erases the annotation of a `const`/`let`/`var` declaration whose first error is the missing initializer, and the machine-readable file is `repair-table.v2.json`. Auto-repair is off by default and syntax-only. It runs only on source that fails to parse. A fix is applied only when it is the single plausible one and the repaired source parses; any fix that could change meaning is reported as a suggestion instead. Applied repairs are listed first in the report and the repaired source is returned. **Ranking.** Entries are ranked by how often code written by LLMs makes each mistake. Entries marked `observed` have been seen in such code; entries marked `prior` have not been seen yet and are kept because they are cheap, unambiguous and commonly expected. Priors rank below observed entries and are revisited when evidence arrives. The machine-readable form is `testdata/repair-table.v2.json`; the test cases are under `testdata/golden/`. ## How a repair runs Entry point: `SyntaxRepair.Repair(source, parseOptions, repairOptions, ct)` (also `ScriptEngine.CheckAndRepair`); the CLI is `oneecma check --repair [--out ] `. Nothing in the parser repairs anything: `JsParser.Parse` and `ScriptEngine.Check` report errors only. 1. If the first or last non-blank line is only a fence (R003), the fence is stripped provisionally. A source that parsed is kept stripped only if the rest parses too. 2. While the text has errors, take the first one (by position) and ask every apply rule for candidate fixes near it (its line and the line before). Each candidate is applied to a copy and parsed. It is plausible when the copy parses or its first error lies beyond the edit and the token after it. 3. A candidate is applied only if it is the single distinct plausible result. Rival readings (R001 "insert an opener instead") count against it but are never applied. No candidate, or more than one, ends the search. 4. Repeat, to `MaxRepairs` (default 16) repairs and `MaxParses` (default 400) trial parses. Hitting either ends the search with a `repair-limit` warning. 5. All or nothing: if the final text still has an error, nothing is returned repaired; the result carries the original diagnostics with their suggestions. If the text parses, the result lists the repairs in the order applied (offsets in the original source) and the repaired source. A repair is one rule application. Rules that fix a recurring mistake at once (R001 on a run of closers, R007 on a signature, R009, R010, R011) make all their edits in one repair; the report block points at the first. Block layout: ``` repair[R001]: removed an extra closing delimiter --> case.js:3:26 | 3 | console.log(items.length)); | ^ removed | = note: the repaired source parses and is returned with this report ``` The closing note is `RenderOptions.RepairOutcome`; a host that goes on to run the repaired source may word it differently. The library only parses. Suggest-only entries are ordinary diagnostics: the explanation as `help:`, the reason as `= note: not repaired automatically: `, and where a mechanical edit exists a `SuggestedFix` in `Diagnostic.Fixes` (S-CLOSE, S-STRING, S-AWAIT, S-REDECL for `let`/`var`, S-PY, S-TRY, S-BACKTICK). S-TRY carries no note (the author writes the handler), S-RETURN and S-MODULE carry none because no edit is mechanical. ## Table | rank | id | mistake | action | evidence | seed cases | |---:|---|---|---|---|---| | 1 | S-BACKTICK | backtick inside a template literal (for example in a comment inside a CSS or HTML template) | suggest | observed | `syn-027-backtick-in-template` | | 2 | S-AWAIT | top-level `await` in a script | suggest | observed | `syn-017-top-level-await` | | 3 | S-REDECL | redeclared `let`/`const`/`class` at top level (often across repeated evaluations in one global scope) | suggest | observed | `syn-022-redeclared-const` | | 4 | R001 | extra closing delimiter `)` `]` `}` after a complete expression or statement, alone or as a run (`)))`, `}}}`) | apply | observed | `syn-001-extra-close-paren`, `syn-002-extra-close-bracket`, `syn-037-doubled-extra-closer` | | 5 | S-CLOSE | missing `)` / `]` / `}` in the middle of the source | suggest | observed | `syn-003-missing-close-paren` | | 6 | R007 | TypeScript type syntax in JavaScript: parameter/variable annotations, `as T`, `interface`/`type` declarations | apply | observed | `syn-019-ts-parameter-types`, `syn-020-ts-as-cast`, `syn-021-ts-interface`, `syn-036-ts-variable-annotation` | | 7 | R012 | `)` or `]` missing right before a `;` that ends the line (`foo(1;`) | apply | observed | `syn-038-missing-paren-before-semicolon`, `syn-039-two-missing-parens-before-semicolon` | | 8 | R002 | closing brackets missing at the very end of the input (truncated output) | apply | prior | `syn-004-unclosed-brace-at-end` | | 9 | R003 | markdown code fence lines around the program | apply | prior | `syn-034-closing-fence-only`, `syn-007-fenced-block-runs-as-templates` | | 10 | R004 | typographic quotes used as string delimiters | apply | prior | `syn-008-smart-double-quotes`, `syn-009-smart-single-quotes` | | 11 | R005 | missing comma between elements, properties or arguments on separate lines | apply | prior | `syn-010-missing-comma-object`, `syn-011-missing-comma-array`, `syn-012-missing-comma-arguments` | | 12 | R006 | Python keyword or operator in place of the JavaScript one: `elif`, `and`, `or` | apply | prior | `syn-013-python-elif`, `syn-016-python-and-or` | | 13 | R008 | comma between class members | apply | prior | `syn-026-comma-between-methods` | | 14 | R009 | backslash before a quote outside a string (JSON-escaped code pasted raw) | apply | prior | `syn-028-escaped-quotes` | | 15 | R010 | HTML entities in code: `&`, `<`, `>`, `"` | apply | prior | `syn-030-html-entities` | | 16 | R011 | literal backslash-n between statements | apply | prior | `syn-029-literal-backslash-n` | | 17 | S-RETURN | top-level `return` | suggest | prior | `syn-018-top-level-return` | | 18 | S-STRING | unterminated string, or a literal line break inside a string | suggest | prior | `syn-005-unclosed-string`, `syn-006-newline-in-string` | | 19 | S-PY | other Python-isms: `def`, `for x in range()`, f-strings, `None/True/False` | suggest | prior | `syn-014-python-def`, `syn-015-python-fstring`, `syn-032-python-for-in-range`, `run-006-python-true` | | 20 | S-MODULE | `import`/`export` in a script | suggest | prior | `syn-023-import-in-script` | | 21 | S-TRY | `try` without `catch`/`finally` | suggest | prior | `syn-031-try-without-catch` | ## Rules and guards ### S-BACKTICK (suggest) - Mistake: backtick inside a template literal (for example in a comment inside a CSS or HTML template). - Rule: escape the inner backtick. - Guard: the missing delimiter could instead be a closing backtick. - Observation: an unescaped backtick inside a template literal. ### S-AWAIT (suggest) - Mistake: top-level `await` in a script. - Rule: wrap the code in an async function and call it, or run it as a module. - Guard: wrapping changes the scope of every declaration and the completion value; a host may instead choose async-function-body mode for the whole call. - Observation: top-level await is the most frequent syntax-level mistake. ### S-REDECL (suggest) - Mistake: redeclared `let`/`const`/`class` at top level (often across repeated evaluations in one global scope). - Rule: rename, assign instead of redeclaring, or evaluate each call in a fresh scope. - Guard: every fix changes meaning. - Observation: redeclaring let/const between evaluations in one global scope. ### R001 (apply) - Mistake: extra closing delimiter `)` `]` `}` after a complete expression or statement, alone or as a run of several (`)))`, `}}}`). - Rule: delete the one closing token the parser reports as unexpected; when several unmatched closers follow it directly, delete the run in one repair (`console.log(1)));` -> `console.log(1);`). - Guard: apply only if (a) the token is a closer with no matching opener, (b) deleting it parses and the first error moves past it, and (c) no rival reading parses: inserting the matching opener at the start of the statement (for `)` only when the wrapping would change the meaning, since parenthesising a whole expression statement means the same thing; for `]` always) or, for `}`, where a body without braces could begin (after `)`, `else`, `=>`, `do`, `try`, `finally`); otherwise suggest. For a run, each length is tried and only the length that makes the source parse is plausible; the opener rival is checked for the first closer only, so a run of `]` still falls back to a suggestion - Example: `console.log(items.length));` -> `console.log(items.length);` - Observation: extra closing delimiters are the one syntax mistake seen repeatedly; only some are fixed by deleting the reported token. ### S-CLOSE (suggest) - Mistake: missing `)` / `]` / `}` in the middle of the source. - Rule: insert the closer where the indentation or the next statement suggests. - Guard: more than one position parses. - Observation: a missing closer in the middle of a call. ### R007 (apply) - Mistake: TypeScript type syntax in JavaScript: parameter/variable annotations, `as T`, `interface`/`type` declarations. - Rule: erase the type syntax (annotations, `as` casts, interface and type-alias declarations); runtime meaning is unchanged. - Guard: only for the erasable subset (no enum, valueful namespace, parameter properties, decorators); apply only if the erased source parses; the annotations of one parameter list and its return type are one repair, and a chain `x as A as B` is one repair; code that is meant to be TypeScript is out of scope - Example: `function add(a: number, b: number) {` -> `function add(a, b) {` - Observation: type annotations appear in code meant for a plain JavaScript host. ### R012 (apply) - Mistake: `)` or `]` missing right before a `;` that ends the line (`foo(1;`). - Rule: insert the closers of every group opened on that line and still open at the `;`, innermost first, directly after the last token before the `;`. - Guard: apply only if the `;` is the last thing on its line (a trailing comment is fine), every group to close was opened on that same line and is a `(` or `[` that is not a `for (` header, and the result parses. Text after the `;` on the line, or an opener on an earlier line, means the closer position is a guess; the mistake then stays a suggestion (S-CLOSE). - Example: `const result = double(21;` -> `const result = double(21);`; `console.log(double(21;` -> `console.log(double(21));` - Observation: the narrow, unambiguous case of the observed missing `)` in the middle of a call. ### R002 (apply) - Mistake: closing brackets missing at the very end of the input (truncated output). - Rule: append the closers for the open-bracket stack, innermost first, at end of input. - Guard: apply only if every parse error is at the end of the input (end of input inside open brackets, including a call or array cut off mid-list) and the stack is non-empty; never close an open string or template literal; indentation is evidence: if a later line is indented no deeper than the line of an open `{` and is not itself a closer, that `{` was left open in the middle of the file and nothing is applied; closers go innermost first, a `}` on its own line at the indentation of its opener's line, `)` and `]` directly after the last token - Example: `function f() { ⏎ return 1;` -> `function f() { ⏎ return 1; ⏎ }` ### R003 (apply) - Mistake: markdown code fence lines around the program. - Rule: remove lines that consist only of ``` or ``` at the start or end of the input. - Guard: a fully fenced block already parses (the backticks form template literals), so this check runs before parsing: if the first or last non-blank line is only a fence, strip it; apply only if the result parses (when the source did not parse to begin with, the other rules may finish the job and the whole repair must parse) - Example: ````js ⏎ const n = 5; ⏎ ```` -> `const n = 5;` ### R004 (apply) - Mistake: typographic quotes used as string delimiters. - Rule: replace U+2018/U+2019 with ' and U+201C/U+201D with " when they open or close a string where the parser expects a token. - Guard: only outside strings, comments, template literals and regular expressions ; apply only if the result parses; pairs must match (open/close). - Example: `console.log(“hello”);` -> `console.log("hello");` ### R005 (apply) - Mistake: missing comma between elements, properties or arguments on separate lines. - Rule: insert `,` after the previous element when the next line starts a new element. - Guard: the innermost open bracket is `[`, the `(` of a call or the `{` of an object literal (not a block); the previous token ends a value and the next one, on a new line, starts a value or property; inserting there makes the first error move on - Example: `host: "x" ⏎ port: 1` -> `host: "x", ⏎ port: 1` ### R006 (apply) - Mistake: Python keyword or operator in place of the JavaScript one: `elif`, `and`, `or`. - Rule: `elif` -> `else if`; `and` -> `&&`; `or` -> `||`. - Guard: only when the word is the reported unexpected identifier, sits where an operator or `else if` belongs, and the result parses; `not`, `def`, `in range`, `:` blocks, `None/True/False` are not mechanical (see S-PY). - Example: `} elif (n > 1) {` -> `} else if (n > 1) {` ### R008 (apply) - Mistake: comma between class members. - Rule: delete the comma. - Guard: only directly between two class elements; result parses. - Example: `inc() {}, ⏎ dec() {}` -> `inc() {} ⏎ dec() {}` ### R009 (apply) - Mistake: backslash before a quote outside a string (JSON-escaped code pasted raw). - Rule: remove the backslash. - Guard: only when the lexer rejects a backslash that precedes a quote; every escaped quote from there to the end of that line is unescaped in one repair; apply only if the result parses - Example: `console.log(\"hi\");` -> `console.log("hi");` ### R010 (apply) - Mistake: HTML entities in code: `&`, `<`, `>`, `"`. - Rule: decode the entity. - Guard: only outside strings, comments and template text; every entity in code is decoded in one repair; apply only if the decoded source parses - Example: `a && b` -> `a && b` ### R011 (apply) - Mistake: literal backslash-n between statements. - Rule: replace the two characters `\n` with a line break. - Guard: only outside strings, templates, regular expressions and comments, where `\` cannot start a token; every such `\n` is replaced in one repair; apply only if the result parses - Example: `const a = 1;\nconst b = 2;` -> `const a = 1; ⏎ const b = 2;` ### S-RETURN (suggest) - Mistake: top-level `return`. - Rule: put the code in a function, or end with the value as an expression. - Guard: the intended result depends on the host. ### S-STRING (suggest) - Mistake: unterminated string, or a literal line break inside a string. - Rule: close the string, use `\n`, or use a template literal. - Guard: the fix choice changes the string. ### S-PY (suggest) - Mistake: other Python-isms: `def`, `for x in range()`, f-strings, `None/True/False`. - Rule: translate by hand. - Guard: no mechanical rewrite keeps the meaning; `None/True/False` parse and fail at run time as undefined names. ### S-MODULE (suggest) - Mistake: `import`/`export` in a script. - Rule: run as a module. - Guard: module mode changes strictness and `this`. ### S-TRY (suggest) - Mistake: `try` without `catch`/`finally`. - Rule: add a catch clause. - Guard: the handler body is the author's. ## Notes - Indentation is admissible evidence for where a missing closing bracket goes (R002, S-CLOSE): a dedent to the column of the opener marks the position. - Top-level `await`, top-level `return` and redeclaration between evaluations are better handled by a host evaluation mode (async function body, fresh scope per call) than by repair; they stay suggestions. # Source maps (ECMA-426) Reports about minified, bundled or transpiled code point at the source the author wrote. The consumer follows ECMA-426 (`https://tc39.es/ecma426/`); this page lists what it does where the specification leaves room, and how reports use it. Types live in `RecursiveAI.OneEcma.SourceMaps`. ## Decoding (`SourceMap.Parse`) Input is the map text, an optional base URL (where the map was loaded from) and limits. It never throws for bad input: it returns a `SourceMapParseResult` with the `Map` (null when nothing usable could be decoded) and a list of problems, each `{ code, message, path, fatal }`. `ToDiagnostics()` turns problems into warning diagnostics of kind `SourceMap` over the map text, so they render like any report. - **Standard maps** (section 9) and **index maps** with `sections` (section 10). `sources`, `sourcesContent`, `names`, `sourceRoot`, `file` and `ignoreList` are read as the specification's `GetOptional...` operations; other properties are ignored (9.4). - **Mappings** are decoded with the VLQ rules of section 6 (values below 2^32 before the sign is applied; continuation digits that add nothing are allowed) and the segment grammar of 9.2.1 (1, 4 or 5 fields; empty groups allowed; an empty segment is not). - **Sorting.** Mappings are sorted by generated position, ties keep map order (9.1.2). Index-map sections are merged: sources that are equal (URL, content, ignored) become one, names are concatenated, the section offset moves line 0's columns and every line (10.1). - **Source URLs.** `sourceRoot` is prefixed with a `/` added when missing (9.3.1); the result is resolved against the base URL with `SourceMapUrl.Resolve` (relative references fold `.` and `..`; a reference with a scheme is kept; with no base, the text is kept as written). - **Optional errors** of the specification are problems that leave the map usable (wrong `version`, a field of the wrong type, a mapping whose source or name index is out of range, which then has no original). **Thrown errors** are fatal problems (not JSON, not an object, `mappings` not a string, `sources` not an array, a malformed index-map section header) and leave `Map` null. ### Decisions where the specification is silent or optional - **A grammar error in `mappings`** (bad character, wrong field count, VLQ over 32 bits, empty segment) stops decoding at that point and keeps the mappings decoded before it. The specification would return no mappings. The problem is reported either way, `IsValid` is false, and a half-working map still places most errors correctly. - **`sourceRoot` of `""`** counts as absent (the literal rule would turn `a.js` into `/a.js`). - **Index map with a top-level `mappings` or `sources`** is reported, and an index-map section that starts at or before the position of the previous section's last mapping is reported as an overlap (the conformance data treats both as invalid). - **`ignoreList`** entries with no matching source are reported. - **Limits** (`SourceMapLimits`): 64 million characters of JSON, JSON depth 32, 4 million mappings (the rest are dropped, one problem), 100 000 sources, 10 000 sections, 100 problems recorded. Exceeding size, depth, sources or sections is fatal. ## Lookup (`GetOriginalPositions`, `TryGetOriginalPosition`) Generated line and column are zero-based, columns in UTF-16 code units. The applying mapping is the last one at or before the position in line-then-column order (12.1) — it may be on an earlier line — and every mapping at exactly that generated position contributes, in map order. Entries without an original source (one-field segments) are omitted, so an unmapped position gives an empty list. A result carries the source, original line and column, the mapping's name and the matched mapping's generated position. ## Finding the map (`SourceMapUrl`, `SourceMapLoader`) - `SourceMapUrl.Extract` is the "without parsing" extraction of 11.1.2.1, scanning lines from the end: blank lines and comment lines are skipped upward, the first matching `//# sourceMappingURL=` (or `//@`) wins, anything else stops with none, and a comment containing a quote, a backtick or `*/` is ambiguous and gives none. - `data:` URLs (base64 or percent-encoded UTF-8) are decoded by `SourceMapUrl.TryDecodeDataUrl`. - `SourceMapLoader.Load` returns null when there is no comment or the map cannot be obtained. Any other URL is resolved against the generated file's name and handed to the host's `SourceMapFetcher`; the engine reads no files and opens no connections. A fetched text starting with `)]}'` loses its first line (11.2.1). ## Reports (`SourceMapReports.Remap`, `RemapAll`) For each diagnostic the primary span is looked up at its start. No mapping: the diagnostic is returned unchanged. - **Range.** A mapping is a point, so the range is rebuilt. The start is the mapping's original position, plus the distance inside the mapping when the mapping has no name (code between mappings is assumed copied as written). The end comes from the last code unit of the span the same way; a mapped name gives the token length (the generated identifier may be renamed); with no usable end the span is the name length or the generated length. - **Original text available** (from `sourcesContent`, or from the host's resolver by URL): the diagnostic is rebuilt over the original `SourceText` named by the source URL. Secondary labels that map into the same source stay labels; those that map elsewhere or nowhere become notes naming the other place or the generated position. Help lines that quote code get " (in the generated code)". The rest of a runtime report is carried over: every stack frame of the generated source moves to the original source (its span and the position `err.stack` reports; locals, async and construct flags travel with it; a frame with no mapping, a host frame and a frame of another source stay as they are), provenance steps move like secondary labels (a step that maps elsewhere or nowhere becomes a note), and the did-you-mean suggestions, which hold no positions, are kept. Suggested fixes and recovery steps are dropped. A note `generated position: file:line:col` is added and `Diagnostic.Mapping` records both places (JSON `mapping`). - **Original text not available:** the diagnostic keeps the generated source, with its stack, provenance and suggestions unchanged, and gains a note `original position: file:line:col (the original source text is not available)`, and `Mapping.SpansAreOriginal` is false. ## CLI `oneecma check ` follows a trailing `data:` source map comment, or a map file next to the script named by the comment, only when there is something to report; `--source-map ` names the map explicitly. Original sources missing from `sourcesContent` are read from the path the map names. A map that cannot be used adds warnings to the report and never changes the exit code. Repaired source (`--repair`) is not remapped. ## Tests `tools/source-map-tests/fetch.sh` fetches the pinned `tc39/source-map-tests` data (BSD-3-Clause, never committed); set `ONEECMA_SOURCE_MAP_TESTS` to the directory and `SourceMapSpecTests` runs every validity case, every mapping, ignore-list and transitive-mapping action and the decoding goldens. Without it those tests skip. Our own cases are in `SourceMapTests` and `testdata/golden/map-001-*`; the decoder has a fuzz target (`oneecma-fuzz sourcemap`). # Record log (format version 1) A record log holds everything a run took from outside the engine, in order, so the run can be replayed in a fresh engine and give the same values, exceptions and report bytes. Recording is on by default (`ScriptEngine.RecordLog`, or pass your own through `ScriptEngineOptions.Log`); it costs a few appends per host interaction and nothing per instruction. ## What is recorded | Event | Recorded when | Payload | |---|---|---| | `Eval` (1) | `ScriptEngine.Evaluate` (outermost call) | source, file name | | `Module` (2) | `EvaluateModule` | specifier | | `Jobs` (3) | `RunJobs` | none | | `Clock` (4) | `IJsHost.CurrentTimeMs` | kind byte; 0 = zigzag varint difference from the previous reading, 1 = raw double | | `Mono` (5) | `IJsHost.MonotonicMs` read by script (`performance.now`) | double. Limit polling is not recorded: how often it runs depends on fuel, not on the script. | | `Random` (6) | `IJsHost.NextRandom` | double | | `ZoneId` (7), `Locale` (8) | first read of `TimeZoneId`, `DefaultLocale` | string | | `ZoneOffset` (9) | `TryGetTimeZoneOffset` | zigzag varint offset in ms, flags byte (1 known, 2 daylight) | | `Resolve` (10) | `ResolveModule` | byte 0 none, or 1 then module id and source | | `HostCall` (11) | a host function returned or threw | member name, outcome | | `Settle` (12) | a host task settled | call id, outcome | | `Weak` (13) | a weak-reference collection point | count, ids | | `EntryEnd` (14) | an entry call ended | 0, or 1 and the headline of the error | | `DefineFn` (15) | `DefineGlobalFunction` | name, length | | `AsyncBody` (16) | `ScriptEngine.EvaluateAsyncBody` (outermost call) | source, file name | | `Await` (17) | `ScriptEngine.Await(Async)` on the promise an earlier entry call returned | index of that entry call (the first is 0) | | `Callback` (18) | .NET code calls a script function (`JsRealm.Call`, `ScriptEngine.Call`, the callbacks of `[JsExport]` methods) | function number, receiver, argument count, arguments (see below) | | `CallbackEnd` (19) | the callback written as `Callback` inside a host function returned or threw | byte: 0 returned, 1 threw | Outcome byte of `HostCall` and `Settle`: 0 value, 1 .NET exception (type name, full type name, message, frame count, then function, file, line per frame), 2 thrown script value, 3 opaque (the result is not data: a function, a host object), 4 task started (call id; `HostCall` only), 5 the error thrown by a callback of this call escaped the host function unchanged (no payload; `HostCall` only). ### Callbacks into script Objects and functions that script hands to a host function are numbered in the order they are first seen: at the entry of every host function the engine numbers its receiver and arguments, and one level below, the members of plain objects and arrays (at most 1024 objects, at most 64 members looked at per object). A recording and its replay number them the same way, so a callback names its function by that number. The receiver and each argument of a callback are written as a kind byte, then either 0 and the value as data (as for host results), or 1 and an object number, so a callback that changes an object script passed in changes that same object in the replay. A callback made while a host function runs is written as `Callback`, whatever happens inside the callback, and `CallbackEnd`, before the `HostCall` event of the host function. Replay does not run the host function: it reads the `Callback` events that come first, runs each one, and then reads the host call's outcome. If the outcome was opaque the host function runs live and is handed the results of those callbacks instead of calling script again. A callback made when no script is running (an event handler fired by the host, the continuation of an async host method after an `await`) is an entry call of its own: `Callback`, the events of its run, and `EntryEnd`; it counts in the entry numbering used by `Await` and `StopAfterEntry`. Values are copied by structure: undefined, null, booleans, numbers (double), strings (UTF-8), BigInts (decimal text), arrays (length, then index and value of each element, so holes survive) and plain objects (key and value pairs in property order). An object met twice is written once and referred to by its creation index, so cycles and sharing survive. Anything else makes the result opaque; replay then calls the live host function, which must be defined (in `ScriptEngineOptions.Configure`). ## Encoding Header: the four bytes `OERL`, then the version byte. Events follow back to back: one tag byte, then the payload. Integers are LEB128 varints (zigzag for signed), doubles are 8 bytes little-endian, strings are a varint (byte count shifted left by one, low bit set when the string holds a lone surrogate) and UTF-8, or WTF-8 (each lone surrogate as its own three bytes) when the low bit is set. A reader refuses another version, unknown tags (tags 16 to 19 and outcome 5 were added without a version change, so a reader from before them refuses logs that use them), truncation and values nested deeper than 64 levels with `FormatException`. ## Replay `ScriptEngine.Replay(log, options)` builds a fresh engine whose host answers from the log and never touches the real host, then performs the recorded entry calls in order. Host functions are not executed: their recorded results are returned, so side effects of the .NET code do not happen again. The replay checks every event it reads against what the script asked for and stops with `ReplayResult.Divergence` when they differ; it also compares the outcome of each entry call with the recorded one. A task started by a host function (`JsMarshal.FromTask`) becomes a pending promise in the replay; its `Settle` event, found between entry calls, settles it, so continuations run at the recorded place in the job order. The `Settle` event always follows the `HostCall` event that started the task, also when the task was already complete (a completed, faulted or cancelled task, or one that throws before it returns) and settled inside the call. `Await` marks the awaited promise as handled in the replay, as the wait did in the recording, so the job queue treats a rejection the same way in both. A rejection the host sees through `Await`/`AwaitAsync` is explained from a replay of the whole log; when the promise was not returned by an entry call, the report says so. Timers are host-side: a host that fires timers by evaluating a script (`engine.Evaluate("fire()")`) replays exactly, because each firing is an entry call. ## Not replayable The log is marked not replayable (`RecordLog.IsReplayable`, `NotReplayableReason`) when the run called a script function from .NET that script never passed to a host function (it has no number), or with a receiver or argument that is neither data nor such an object, or from a place where no replay could re-enter (script code that is not a host function, for example a validating wrapper made by the JSON-Schema helper), or used `JsRealm.Construct`, `SetProperty`, a second realm, a host task that settled with a non-data value or another promise, or when the log outgrew `MaxBytes` (default 8 MiB). A host that builds objects the script uses (data, `[JsExport]` installs) must do it in `Configure` so a replay rebuilds the same world. ## Size A run with no host interaction logs its script source plus about 20 bytes. `Date.now()` in a loop costs 2 to 3 bytes a reading, `Math.random()` 9 bytes, a host call 3 to 12 bytes plus its data. # Array and iterator built-ins Implementation notes for `Array`, the array iterators, `Iterator` and the iterator helpers. The algorithms follow ECMA-262 (17th edition) step by step; this page records only what the specification leaves open or what differs from it on purpose. ## Where the specification leaves room - **Cyclic `join`/`toString`/`toLocaleString`.** An array that is already being joined contributes the empty string (`a = [1]; a.push(a); String(a) === "1,"`), the behaviour of Node. Nesting deeper than 1000 levels, or a host stack running low, is a `RangeError: Maximum call stack size exceeded`. - **`sort` comparator order.** The sort is a stable bottom-up merge sort. A comparison passes the element from the right-hand run first, so a degenerate comparator such as `() => -1` reverses the input and `() => 1` keeps it, as in Node. The result for a comparator that contradicts itself is always a permutation of the input. - **`toLocaleString`.** Each element's `toLocaleString` is called with exactly two arguments, `locales` and `options` (ECMA-402 behaviour). The base-language test that expects no arguments only holds in a build without `Intl`. - **`flat` and `flatMap`.** Nesting is walked with an explicit stack that is capped at the call-depth limit; a cyclic array flattened with `Infinity` ends in a `RangeError`. - **`take`/`drop`.** A finite limit above 2^53 - 1 is a `RangeError`, as the conformance suite requires (the edition text only rejects `NaN` and negative limits). - **Dense fast paths.** `push`, `unshift`, `shift`, `splice`, `reverse` and `slice` copy a dense array's storage directly only when the result is the same as the specification's element-by-element algorithm: every property is a default data property, the array is extensible with a writable length, and no object on the prototype chain can hold or intercept an integer key. Anything else takes the generic path. ## Huge and sparse lengths Loops charge one unit of fuel per index, so a loop over `length: 2**53 - 1` ends in a fuel limit report. For `indexOf`, `lastIndexOf`, `includes` (with a defined search value), `sort` and the deletion of leftover slots after `sort`, a range longer than 65536 indices on an ordinary object or array whose prototype chain holds no integer keys visits only the keys that exist. If a getter changes the object during such a visit, indices added after the walk started are not seen; this differs from the specification only for objects that mutate themselves from accessors on a length above 65536. Results that must be materialised (`toSorted`, `toReversed`, `toSpliced`, `with`) charge their size to the allocation budget before they allocate, and a length above 2^32 - 1 is a `RangeError` (`Invalid array length`). ## V8 wording | Script | `err.message` in Node v24.22.1 | |---|---| | `[].map(1)` | `number 1 is not a function` | | `[].map({})` | `object is not a function` | | `[].forEach('x')` | `string "x" is not a function` | | `[].filter(null)` | `object null is not a function` | | `[].some(undefined)` | `undefined is not a function` | | `[].reduce((a, b) => a)` | `Reduce of empty array with no initial value` | | `Array.from(null)` | `object null is not iterable (cannot read property Symbol(Symbol.iterator))` | | `new Array(-1)`, `Array(2**32)` | `RangeError: Invalid array length` | | `[1].sort(1)` | `The comparison function must be either a function or undefined: 1` | | `[1].with(5, 1)` | `RangeError: Invalid index : 5` | | `Array.prototype.map.call(undefined, f)` | `Array.prototype.map called on null or undefined` (also `concat`, `indexOf`, `forEach`, `filter`, `every`, `some`, `reduce`, `reduceRight`, `find`, `findIndex`, `findLast`, `findLastIndex`) | | `Array.prototype.join.call(null)` | `Cannot convert undefined or null to object` (all other methods) | | `Array.prototype.push.call({length: 2**53 - 1}, 1)` | `Pushing 1 elements on an array-like of length 9007199254740991 is disallowed, as the total surpasses 2**53-1` | | `Object.freeze([1]).pop()` | `Cannot delete property '0' of [object Array]` | | `Object.freeze([]).push(1)` | `Cannot add property 0, object is not extensible` | | `Object.defineProperty([], 'length', {writable: false}).push(1)` | `Cannot assign to read only property 'length' of object '[object Array]'` | | `[][Symbol.iterator]().next.call({})` | `Method Array Iterator.prototype.next called on incompatible receiver #` | | `Iterator()` | `Constructor Iterator requires 'new'` | | `new Iterator()` | `Abstract class Iterator not directly constructable` | | `Iterator.prototype.map.call(1, f)` | `Iterator.prototype.map called on non-object` | | `Iterator.prototype.take.call({}, -1)` | `RangeError: -1 must be positive` | | `Iterator.from(1)` | `Iterator.from called on non-object` | | `Iterator.prototype.reduce.call({next() { return {done: true}; }}, f)` | `Reduce of a done iterator with no initial value` | | a running helper's `next()` called re-entrantly | `Generator is already running` | Known differences from Node: `Object.freeze([1]).sort()` throws a `TypeError` here (the specification stores the element back) where Node returns the array; the message for a non-callable `Symbol.iterator` on `Iterator.from` is the engine's general `is not a function` wording. # Binary data: ArrayBuffer, SharedArrayBuffer, TypedArrays, DataView, Atomics Normative notes for the binary-data built-ins (ECMA-262 17th edition, clauses 23.2, 23.3, 25.1-25.4). Where the specification leaves room, this file says what the engine does. ## Representation - A buffer is one `byte[]` holding exactly the current byte length. Resizing (`resize`, `grow`) replaces the array and copies; views refer to the buffer object, so this is unobservable. Detaching replaces it with an empty array and sets a flag. - Element access is `BinaryPrimitives` over a span of that array: little-endian for typed arrays (the host byte order is not observable), explicit endianness for `DataView`. Numbers never box; a BigInt element allocates only the `BigInteger`. - A typed array stores the buffer, the byte offset and a fixed length, or "auto" for a length-tracking view of a resizable buffer. Length and out-of-bounds status are recomputed on every access, so a resize is seen at once. - Integer-indexed keys never reach property storage and never consult the prototype chain (10.4.5). A Number key (`ta[i]`) takes a fast path in the interpreter; string keys go through `CanonicalNumericIndexString`. - `Float16Array`, `DataView.prototype.getFloat16`/`setFloat16` and `Math.f16round` use `System.Half`, which rounds to nearest even from a double. ## Limits - The largest buffer is 2,147,483,591 bytes (the .NET array limit). A larger request, or a `maxByteLength` above it, is `RangeError: Array buffer allocation failed`. A length that fails `ToIndex` is `RangeError: Invalid array buffer length` (`Invalid typed array length: N` for typed arrays). - Every allocation (buffer creation, `resize`, `grow`, `transfer`, `slice`, `toSorted`, `toReversed`, `with`, `Uint8Array.from*`) is charged to the engine's allocation budget before it happens. Over budget ends the run with a memory limit, which script cannot catch. A resizable buffer is charged for its current length, not its `maxByteLength`. - Loops proportional to length charge fuel: one step per eight elements for native loops (`fill`, `indexOf`, `reverse`, `set`, copies, base64 and hex), one per element for loops that call script or read from array-likes (`map`, `forEach`, `from`, constructor from an array-like, `set` from an array-like). Sorting with a comparator ticks per comparison. - Result strings (`join`, `toBase64`, `toHex`) above the engine's string limit are `RangeError: Invalid string length`. ## Host-defined behaviour - `IJsHost.CanBlock` (default `true`) is the agent's `[[CanBlock]]`. With `false`, `Atomics.wait` throws `TypeError`. - The engine has one agent. A permitted `Atomics.wait` on a matching value can only time out; it returns `"timed-out"` at once and never sleeps. With an infinite timeout nothing could ever wake it, so it throws `TypeError`. - `Atomics.waitAsync` waiters live in the agent. `Atomics.notify` resolves them with `"ok"` in FIFO order through a queued job. A waiter with a finite timeout times out on virtual time: when the job queue drains, the waiter with the earliest deadline (ties in creation order) resolves `"timed-out"` and the loop continues. A waiter with no timeout stays pending. A run therefore never waits on the wall clock and replays identically. - `Atomics.isLockFree(n)` is true for 1, 2, 4 and 8. - The conformance host's `$262.detachArrayBuffer` detaches an `ArrayBuffer` and returns `null`. `$262.agent` does not exist; test262 cases that start further agents are not run. - `Atomics.pause` is part of the next edition and is not provided. ## Deviations and choices - `DataView.prototype.byteLength`/`byteOffset` on a detached or out-of-bounds view throw `TypeError`, as the 17th edition and Node do. The pinned test262 has tests written for a later text that returns 0; they are listed as documented failures. - `Object.freeze`, `Object.preventExtensions` and `Reflect.preventExtensions` fail for views that can change size (length-tracking, or over a resizable `ArrayBuffer`), as 10.4.5.1 requires. - `%TypedArray%.prototype.toLocaleString` passes its first two arguments on to each element's `toLocaleString`, as ECMA-402 specifies. - Messages for `fromBase64`, `fromHex`, `setFromBase64` and `setFromHex` are the engine's own (Node 24 does not ship these methods): `SyntaxError` with "The input is not valid base64", "The base64 input ends with an incomplete chunk", "The base64 input has non-zero padding bits", "The hex input has an odd length" or "The hex input contains a character that is not a hexadecimal digit". ## V8 message wording Recorded by running Node v24.22.1 (full ICU) on each script; the engine produces the same `err.message`. | Script | Error | |---|---| | `new ArrayBuffer(-1)` | `RangeError: Invalid array buffer length` | | `ArrayBuffer(1)` | `TypeError: Constructor ArrayBuffer requires 'new'` | | `new ArrayBuffer(2**40)` | `RangeError: Array buffer allocation failed` | | `new ArrayBuffer(1, {maxByteLength: 0})` | `RangeError: Invalid array buffer max length` | | `ArrayBuffer.prototype.slice.call({})` | `TypeError: Method ArrayBuffer.prototype.slice called on incompatible receiver #` | | `buffer.transfer(); buffer.slice()` | `TypeError: Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer` | | `rab.resize(3)` above `maxByteLength` | `RangeError: ArrayBuffer.prototype.resize: Invalid length parameter` | | `sab.grow(3)` above `maxByteLength` | `RangeError: SharedArrayBuffer.prototype.grow: Invalid length parameter` | | `new Uint8Array(-1)` | `RangeError: Invalid typed array length: -1` | | `new Uint8Array(buf, 5)` past the end | `RangeError: Start offset 5 is outside the bounds of the buffer` | | `new Uint32Array(new ArrayBuffer(5))` | `RangeError: byte length of Uint32Array should be a multiple of 4` | | `new Uint32Array(new ArrayBuffer(4), 1)` | `RangeError: start offset of Uint32Array should be a multiple of 4` | | `new Uint32Array(new ArrayBuffer(8), 0, 3)` | `RangeError: Invalid typed array length: 3` | | `new BigInt64Array(new Uint8Array(1))` | `TypeError: Cannot mix BigInt and other types, use explicit conversions` | | `new Uint8Array(detachedBuffer)` | `TypeError: Cannot perform Construct on a detached ArrayBuffer` | | `detached.fill(0)` | `TypeError: Cannot perform %TypedArray%.prototype.fill on a detached ArrayBuffer` | | `Uint8Array.prototype.fill.call([])` | `TypeError: this is not a typed array.` | | `Object.getPrototypeOf(Uint8Array)()` | `TypeError: Abstract class TypedArray not directly constructable` | | `ta.set([1,2,3])` into a 2-element array | `RangeError: offset is out of bounds` | | `ta.with(5, 1)` | `RangeError: Invalid typed array index` | | `new BigInt64Array(1).fill(1)` | `TypeError: Cannot convert 1 to a BigInt` | | `ta.sort(1)` | `TypeError: The comparison function must be either a function or undefined: 1` | | `ta.map(1)` | `TypeError: 1 is not a function` | | `ta.findLast(1)` | `TypeError: number 1 is not a function` | | `Uint8Array.of.call(function(){ return new Uint8Array(0) }, 1, 2)` | `TypeError: Derived TypedArray constructor created an array which was too small` | | `new DataView(1)` | `TypeError: First argument to DataView constructor must be an ArrayBuffer` | | `new DataView(new ArrayBuffer(1), 0, 2)` | `RangeError: Invalid DataView length 2` | | `new DataView(new ArrayBuffer(8)).getInt32(6)` | `RangeError: Offset is outside the bounds of the DataView` | | `Atomics.add(new Float64Array(1), 0, 1)` | `TypeError: [object Float64Array] is not an integer typed array.` | | `Atomics.add(new Int8Array(1), 1, 1)` | `RangeError: Invalid atomic access index` | | `Atomics.wait(new Int32Array(4), 0, 0, 0)` | `TypeError: [object Int32Array] is not a shared typed array.` | | `Atomics.wait(new Int8Array(new SharedArrayBuffer(16)), 0, 0, 0)` | `TypeError: [object Int8Array] is not an int32 or BigInt64 typed array.` | # String, Number, Math, JSON, BigInt and the URI functions What these built-ins do where ECMA-262 leaves room, the wording of the errors they raise, and where they differ from the reference engine (Node v24.22.1). The recorded evidence is in `testdata/builtins/` (scripts plus the reference output; `BuiltinsDifferentialTests` replays them). ## Implementation-defined choices | Area | Behaviour | |---|---| | Case mapping | `toUpperCase`/`toLowerCase` use the Unicode 17.0.0 full mappings: `UnicodeData.txt` simple mappings overridden by the unconditional entries of `SpecialCasing.txt`, plus the Final_Sigma rule (Cased, Case_Ignorable from `DerivedCoreProperties.txt`). Language-sensitive entries (`tr`, `az`, `lt`) are not applied. `toLocaleUpperCase`/`toLocaleLowerCase` ignore their arguments until the Intl layer exists. Data: `src/RecursiveAI.OneEcma/Unicode/CaseData.g.cs`, produced by `tools/gen-case-data.py `. | | `localeCompare` | The CLDR root collation at tertiary strength, with no locale argument handling. Canonically equivalent strings compare equal. | | `normalize` | The engine's own UAX #15 implementation (Unicode 17.0.0). | | `toString(radix)` for a Number | Radix 10 is `Number::toString`. Other radices print the exact integer part and the shortest fractional digits that round-trip. | | `parseInt` | Digits are accumulated exactly and rounded once, for every radix. | | `toFixed`, `toExponential`, `toPrecision` | Exact integer arithmetic on the double's mantissa and exponent; ties pick the larger `n`, as the spec says. | | `Math` functions | The BCL's `Math.*` for sin, cos, tan, asin, acos, atan, atan2, exp, log, log10, log2, sinh, cosh, tanh, asinh, acosh, atanh; `expm1`, `log1p`, `hypot` (scaled, compensated), `cbrt` (one corrected Newton step) are written in this engine. Results are within 2 ulp of Node's. | | `Math.f16round`, `Math.sumPrecise` | `f16round` (ES2025) rounds to binary16 with ties to even. `sumPrecise` sums exactly (the sum is kept as an integer multiple of 2^-1074) and rounds once. | | `JSON.stringify` depth | No limit; the serializer keeps its own stack. Nesting is bounded by memory, the string-length limit and fuel. | | `JSON.parse` depth | No limit; the parser and the reviver walk keep their own stacks. | ## Limits - Every built-in that builds a string checks the result length against `EngineLimits.MaxStringLength` before allocating and throws `RangeError: Invalid string length`: `repeat`, `padStart`, `padEnd`, `concat`, `String.raw`, the HTML methods, `toUpperCase`, `normalize`, `escape`, `encodeURI*`, `JSON.stringify`. - Loops over input charge fuel in proportion to its length (one unit per 32 code units for scans and case mapping, one per element for lists and JSON values). Strings and arrays built by `repeat`, `padStart`, `JSON.parse` and `BigInt.prototype.toString` are also charged to the allocation budget before they are built. - `BigInt.asIntN`/`asUintN` with a width over 2^30 bits and a value that would need that width throw `RangeError: Maximum BigInt size exceeded`. ## Error wording (V8, recorded from Node v24.22.1) | Script | Error | |---|---| | `String.prototype.trim.call(null)` | `TypeError: String.prototype.trim called on null or undefined` (same pattern for every method; `[Symbol.iterator]` prints without the dot) | | `'a'.repeat(-1)` / `repeat(Infinity)` / `repeat(-1.5)` | `RangeError: Invalid count value: -1` / `Infinity` / `-1.5` | | `'abc'.repeat(2**30)`, `'a'.padStart(2**30)` | `RangeError: Invalid string length` | | `'a'.normalize('x')` | `RangeError: The normalization form should be one of NFC, NFD, NFKC, NFKD.` | | `String.fromCodePoint(-1)`, `(1.5)`, `('x')` | `RangeError: Invalid code point -1`, `1.5`, `NaN` | | `'a'.startsWith(/a/)` (also `includes`, `endsWith`) | `TypeError: First argument to String.prototype.startsWith must not be a regular expression` | | `String.prototype.toString.call(1)` | `TypeError: String.prototype.toString requires that 'this' be a String` (same for `valueOf`, `Number.prototype.*`, `BigInt.prototype.*`) | | `String.raw()` | `TypeError: Cannot convert undefined or null to object` | | `(1).toFixed(101)` | `RangeError: toFixed() digits argument must be between 0 and 100` | | `(1).toExponential(-1)` | `RangeError: toExponential() argument must be between 0 and 100` | | `(1).toPrecision(0)` | `RangeError: toPrecision() argument must be between 1 and 100` | | `(1).toString(1)`, `1n.toString(37)` | `RangeError: toString() radix argument must be between 2 and 36` | | `BigInt(1.5)`, `BigInt(NaN)` | `RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer` | | `BigInt('x')` | `SyntaxError: Cannot convert x to a BigInt` | | `BigInt(Symbol())`, `BigInt(undefined)`, `BigInt.asIntN(1, 1)` | `TypeError: Cannot convert Symbol() to a BigInt`, `... undefined ...`, `... 1 ...` | | `new BigInt(1)` | `TypeError: BigInt is not a constructor` | | `BigInt.asIntN(-1, 1n)` | `RangeError: Invalid value: not (convertible to) a safe integer` | | `2n ** -1n` | `RangeError: undefined must be positive` (as printed by the reference engine) | | `decodeURI('%')`, `encodeURI('\ud800')` | `URIError: URI malformed` | ### JSON.parse `SyntaxError` messages, in the order the parser meets them. `position` is the UTF-16 index, `line`/`column` are 1-based and count `\n` only. | Condition | Message | |---|---| | input ends where a value is expected | `Unexpected end of JSON input` | | a character that cannot start a value, or a literal that stops matching | `Unexpected token 'x', "" is not valid JSON` | | the whole text is `undefined`, `NaN` or `Infinity` | `"undefined" is not valid JSON` | | after `{` something other than a string or `}` | `Expected property name or '}' in JSON at position N (line L column C)` | | after `,` in an object something other than a string | `Expected double-quoted property name in JSON at position N (line L column C)` | | after a key something other than `:` | `Expected ':' after property name in JSON at position N (line L column C)` | | after an object value | `Expected ',' or '}' after property value in JSON at position N (line L column C)` | | after an array element | `Expected ',' or ']' after array element in JSON at position N (line L column C)` | | text after the value | `Unexpected non-whitespace character after JSON at position N (line L column C)` | | `-` without digits | `No number after minus sign in JSON at position N (...)` | | a digit after a leading `0` | `Unexpected number in JSON at position N (...)` | | `1.` without digits | `Unterminated fractional number in JSON at position N (...)` | | `1e` / `1e+` without digits | `Exponent part is missing a number in JSON at position N (...)` | | string not closed | `Unterminated string in JSON at position N (...)` (position = text length) | | raw control character in a string | `Bad control character in string literal in JSON at position N (...)` | | `\x` | `Bad escaped character in JSON at position N (...)` (N = the character after the backslash) | | `\u` with a non-hex digit | `Bad Unicode escape in JSON at position N (...)` (N = the first bad digit) | Expected-token messages are used even at the end of input; only the "value expected" state reports `Unexpected end of JSON input`. Snippet: the whole text in double quotes when it is shorter than 21 code units; otherwise 10 code units before the token up to 10 after, quoted, with `...` before when the window does not start at 0 and after when it does not end at the end. ### JSON.stringify `TypeError: Do not know how to serialize a BigInt`. A cycle gives ``` Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'k0' -> object with constructor 'Object' | ... | property 'k3' -> object with constructor 'Object' --- property 'back' closes the circle ``` One `|` line per step from the first object of the cycle; when there are more than three, the first two, `...`, and the last. A step through an array reads `index 0`. The constructor name is the `name` of the data property `constructor` found on the prototype chain, `Object` when there is none. ## Differences from the reference engine - `Number.prototype.toString(radix)` for radices that are not powers of two prints every digit of an integer above 2^53; Node prints zeros after the first 53 bits (`(1e21).toString(7)`). - `parseInt(s, radix)` for radices other than 2, 4, 8, 10, 16, 32 with more than 10 digits is correctly rounded; Node approximates. - `JSON.stringify` of very deep structures succeeds; Node throws `RangeError: Maximum call stack size exceeded` at about 5000 levels. - Transcendental `Math` functions can differ from Node in the last digit (measured over 3000 random arguments, mismatching in the last digit: expm1 22%, log1p 20%, asinh 17%, atanh 15%, atan2 13%, cbrt 8%, exp 4.5%, cos/tan/sin/atan/log/acos/asin about 1-2%). - `toLocaleUpperCase('x_y')` does not validate the locale (Node: `RangeError: Incorrect locale information provided`); validation comes with the Intl layer. - `Math.sumPrecise` exists (ES2026); Node v24 does not have it. # Collections, weak references, Reflect and Proxy Normative notes for the places where ECMA-262 leaves room or where the engine chooses a mechanism. ## Map and Set - Keys are compared with SameValueZero; `-0` is stored as `+0`. Iteration order is insertion order. - The entry list is a linked list. A deleted entry is marked removed and keeps its back link; an iterator parked on a removed entry walks back to a live entry and continues after it. So `delete`, `clear` and re-adding during iteration behave as the specification's "empty slot" list does (an entry deleted and re-added is visited again), and no step is O(n). `clear()` marks every entry removed; entries added afterwards are visited by parked iterators. - `Map.groupBy` and `Object.groupBy` share one grouping helper (`Builtins.GroupBy`): first-seen key order, the callback result goes through ToPropertyKey for objects and `-0` becomes `+0` for maps, the iterator is closed if the callback or the key coercion throws. - The ES2025 Set methods follow the algorithm text step by step, including GetSetRecord's observable order (`size`, `has`, `keys`) and the size comparison that picks which side is iterated. Loops over `this` walk the live list. - ES2026 upsert: `Map.prototype.getOrInsert`, `getOrInsertComputed`, and the WeakMap versions. ## Weak references `WeakMap` and `WeakSet` use `ConditionalWeakTable`, an ephemeron table: a value that refers to its own key does not keep the key alive, and nothing leaks. Their contents cannot be observed, so the collector may act on them whenever it likes. `WeakRef` and `FinalizationRegistry` are observable, so they must not depend on when the .NET collector runs: - A target is held strongly from the moment it is registered. Nothing dies by itself. - A **collection point** is a host decision. The conformance host's `$262.gc()` is one; an embedding host reaches it through the same engine operation. At a collection point the engine releases every target that is not in the kept list to the .NET collector, runs a full collection, marks the targets that were reclaimed as dead, pins the survivors again, and queues one cleanup job per registry that now has dead cells. - Kept objects (9.10): a `WeakRef` construction or successful `deref()` keeps the target until the current synchronous run ends. `ClearKeptObjects` runs when a script finishes and after every job on the queue, and from `$262.clearKeptObjects()`. - Cleanup callbacks run as ordinary jobs on the engine's job queue, one job per registry, dead cells in registration order. A callback that throws leaves the remaining dead cells to a follow-up job. - Record and replay: each weak target gets an id in creation order. The host interface has two default members. `WeakTargetsCollected(ids)` is called at every collection point with the ids found dead (ascending); a recording host logs them. `ReplayWeakCollection()` returns, for a replaying host, the recorded ids; the engine then does not touch the collector and applies exactly that outcome. A run therefore reproduces even though .NET reachability alone would not. - Limitation: references the interpreter itself still holds (a finished frame's stack array that a running frame references, a value in a live local) keep a target reachable. Garbage built by one script run and collected from a later run or job is reliably dead. - The engine never calls the collector on its own; a host that creates many `WeakRef` targets should request collection points. Targets are held strongly until then. ## Proxy - Every internal method follows ECMA-262 10.5 including all invariant checks. A revoked proxy throws a TypeError naming the operation (`Cannot perform 'get' on a proxy that has been revoked`). `IsArray` looks through proxies iteratively. - Callable and constructor status are fixed when the proxy is created from the target, and stay after revocation (`typeof` of a revoked function proxy is `"function"`). - Nesting limit: at most 512 proxy operations may be nested on the host stack; the next throws `RangeError: Maximum call stack size exceeded`. A chain of 100,000 proxies therefore ends in a RangeError, never in a host stack overflow. Traps implemented by script are bounded additionally by the call and native-depth limits. - `Object.prototype.toString` on a revoked proxy says `Cannot perform 'Object.prototype.toString' on a proxy that has been revoked`. - When a trap returns false and the caller must throw (strict assignment, strict `delete`, `Object.defineProperty`, `Object.setPrototypeOf`, `Object.preventExtensions`), the message is `'' on proxy: trap returned falsish for property ''`, as V8 prints it. ## Messages learned from Node v24.22.1 Each wording below was observed by running Node and recording the output. | Situation | `err.message` | |---|---| | `Map()` and the other collection constructors without `new` | `Constructor Map requires 'new'` | | Wrong receiver | `Method Map.prototype.get called on incompatible receiver #` (`get Map.prototype.size` for the accessor) | | `new Map(5)` | `number 5 is not iterable (cannot read property Symbol(Symbol.iterator))` (`object`, `function`, `symbol`, `boolean true` for other kinds) | | `new Map([1])` | `Iterator value 1 is not an entry object` | | Replaced `set`/`add` not callable | `'1' returned for property 'set' of object '#' is not a function` | | `forEach(1)` | `number 1 is not a function` | | `Map.groupBy([], 1)` | `1 is not a function`; `Map.groupBy null` gives `Map.groupBy called on null or undefined` | | Bad WeakMap / WeakSet key | `Invalid value used as weak map key` / `Invalid value used in weak set` | | `new WeakRef(1)` | `WeakRef: invalid target` | | `FinalizationRegistry` | `FinalizationRegistry: cleanup must be callable`, `FinalizationRegistry.prototype.register: invalid target`, `FinalizationRegistry.prototype.register: target and holdings must not be same`, `Invalid unregisterToken ('1')` | | Set methods | `Set.prototype.union argument must be an object`, `The .size property is NaN`, `'-1' is an invalid size` (RangeError), `string "has" is not a function`, `Result of the keys method is not an object` | | Reflect with a non-object target | `Reflect.get called on non-object` | | `Reflect.apply(1)` | `Function.prototype.apply was called on 1, which is a number and not a function` | | `new Proxy(1, {})` | `Cannot create proxy with a non-object as target or handler` | | Proxy trap not callable | `'1' returned for property 'get' of object '#' is not a function`; for `apply` and `construct`: `(intermediate value) is not a function` | | Invariant violations | `'get' on proxy: property 'x' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '1' but got '2')` and the sibling texts in `Builtins/Proxy.cs` | The upsert methods (`getOrInsert`, `getOrInsertComputed`) are not in Node v24, so their errors reuse the neighbouring wording above. ## Known differences from Node - A `with` statement over a proxy calls `has` again when it reads the binding (`GetBindingValue`, 9.1.1.2.6) and when it assigns (`SetMutableBinding`); Node skips the second `has` on reads. The specification's order is followed. # Date Normative description of `Date` as implemented in `Builtins/Date*.cs`: ECMA-262 17th edition 21.4 and Annex B.2.3. ## Host seams - **Clock:** `IJsHost.CurrentTimeMs()` only (`Date.now`, `new Date()`, `Date()`). - **Zone:** `IJsHost.TimeZoneId` names the IANA zone used for local time. - **Offsets:** `IJsHost.TryGetTimeZoneOffset(zoneId, epochMs, out offsetMs, out isDaylight)`. A default interface member answers from the BCL time zone rules of the machine; `DefaultHost` exposes it as a virtual method; a host that must not depend on the machine overrides it with its own rules. When it returns false, local time is UTC. - The BCL rules cover years 1 to 9999. For later instants the engine asks about the same moment in an equivalent 400-year cycle (calendar and weekday repeat exactly; rules do not change that far out). Before year 1 the BCL answers with its earliest offset. ## Local time - `LocalTime(t)` is `t` plus the offset at `t`. - `UTC(t)` for a local time value `t` (21.4.1.26, GetUTCEpochNanoseconds): look at the offsets a day before and a day after `t`; each offset `o` for which the zone's offset at `t - o` is `o` gives a candidate instant; the earliest candidate wins (clocks set back: the first occurrence). With no candidate (clocks set forward) the offset from before the transition is used, which moves the time forward by the size of the gap. - All time arithmetic is exact `double` arithmetic over +/-8.64e15; `MakeDay` returns NaN for years beyond +/-400000 without computing. ## Constructor, `parse`, `UTC` - Every form of 21.4.2.1; `Date()` called as a function returns the string for the current time. A one-argument call with a Date copies its time value without conversion; other objects go through `ToPrimitive`, strings through `Date.parse`. - `Date.UTC` and the multi-argument constructor apply `MakeFullYear` (0 to 99 mean 1900 to 1999). ### Strings accepted by `Date.parse` 1. **The Date Time String Format (21.4.1.32)** exactly: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, expanded years `+YYYYYY`/`-YYYYYY` (`-000000` is rejected), optionally `THH:mm`, `THH:mm:ss`, `THH:mm:ss.s…` (any number of fraction digits, truncated to milliseconds) and `Z` or `±HH:mm` (`±HHmm` too). `T` and `Z` may be lower case. Date-only forms are UTC, date-time forms without an offset are local time. Hour 24 is accepted only as `24:00:00.000`. A day from 1 to 31 is accepted in every month and rolls over (`2023-02-30` is 1 March). 2. **Legacy forms** when the string is not in the format above; observed from Node v24 and kept to the cases real code relies on: - Words: a month name (the first three letters decide: `Jul`, `July`, `Sept`), a day name (ignored), `AM`/`PM` (after a time, hour at most 12), `Z`, `UT`, `UTC`, `GMT`, and the US zone abbreviations `EST EDT CST CDT MST MDT PST PDT`. Any other word is ignored before the first number and makes the string invalid after it. A word touching a number (`4th`, `10:00pm`) is invalid, except a `Z` right after a time. - Numbers: up to three, separated by spaces, `,`, `/`, `.` or `-` (a `-` after a number must touch its neighbours). With a month name: `D Y`, `Y D` (first number above 31, or three or more digits) or one number (a day up to 31, or a year). Without: `M/D/Y`, `Y/M/D` (first number above 31, three or more digits, or 0), `M/D`, `Y/M` or a single month or year. A missing year is 2001, a missing day is 1. Years below 100 written with fewer than four digits mean 2000 to 2049 and 1950 to 1999. - Time: `H:M`, `H:M:S`, `H:M:S.fff`; a colon with no digits after it reads as zero, further `:n` groups are ignored; a dot after the seconds must be followed by digits. - Zone: `+H`, `+HH`, `+HMM`, `+HHMM`, `+HH:MM`, also after `GMT`/`UTC`; up to four digits. Without a zone the result is local time. - Parenthesised text is skipped (an unclosed parenthesis runs to the end); an unmatched `)` or any other character makes the string invalid. - Day 1 to 31 and month 1 to 12 are accepted and day overflow rolls into the next month. - The output of `toString`, `toUTCString` and `toISOString` parses back to the same time value (to the second for `toString` and `toUTCString`). Known differences from the reference: `-000000-01-01` (the reference reads a stray sign, 2001-01-01); a number written with a sign following a number or a zone name is only partly modelled; `Jan 1 1 1` (three numbers and a month, the reference ignores one). ## Prototype All getters and setters of 21.4.4 in UTC and local variants, with the argument conversion order of the specification (every argument is converted before the NaN check). Annex B: `getYear`, `setYear`, `toGMTString` (the same function object as `toUTCString`). `getTimezoneOffset` is exactly `(t - LocalTime(t)) / msPerMinute`, which is fractional where a zone has a seconds offset. `toJSON`, `@@toPrimitive` (hints `default`, `string`, `number`; others throw `TypeError: Invalid hint: x`), `valueOf`. Methods called on a non-Date throw `TypeError: this is not a Date object.` `toISOString` of an invalid date throws `RangeError: Invalid time value`. ## String forms - `toString`: `Thu Jul 04 2024 10:00:00 GMT-0500 (Central Daylight Time)`. The offset is `±HHMM` rounded to the minute; the parenthesised text is the zone's long English name from the CLDR data (`en`), looked up for the instant clamped to 1970 to 9999; a zone without a name there falls back to `GMT-05:00`. Years print with at least four digits and a leading `-` when negative (`-0001`). - `toDateString`: `Thu Jul 04 2024`; `toTimeString`: `10:00:00 GMT-0500 (Central Daylight Time)`; `toUTCString`: `Thu, 04 Jul 2024 10:00:00 GMT`; `toISOString`: `2024-07-04T15:00:00.000Z`, with `±YYYYYY` outside years 0 to 9999. An invalid date prints `Invalid Date` in all but `toISOString`. - `toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`: `DateTimeFormatService` with the locale argument (a string or list of strings, else `IJsHost.DefaultLocale`), the options object read as plain values, and the host zone. U+202F in the CLDR time patterns is replaced by U+0020, as the reference engines print it. ## Differential `testdata/date/probe.js` prints about 2,400 observations (getters, setters, constructor gaps and overlaps, `parse`, locale strings) across DST edges, the epoch, 1900, 2038, the ends of the range and far years. `tools/date-diff/record.sh` records Node's output under `TZ=America/Chicago`, `Europe/Berlin` and `UTC` into `expected.*.txt` (Node v24.22.1). `DateNodeDifferentialTests` runs the same script on the engine with the host zone set to the same zone. Lines for instants before 1900 are compared in UTC only: the zone rules read through the BCL round local mean time to whole minutes and the reference prints seconds (`GMT-0550` against `GMT-0551` in Chicago before 1883). A host with its own tzdata reader does not have this limit. On Windows the default host reads the operating system's zone rules (through CLDR's `windowsZones` mapping when the machine has no IANA ids), which differ from tzdata for the past: Berlin in 1945 has +0200 where tzdata has a second summer hour (+0300). The differential therefore compares Windows runs from 1970 on only; a host that needs tzdata history supplies its own `TryGetTimeZoneOffset`. `testdata/date/deviations.txt` lists the other known differences. # RegExp engine Normative description of the pattern parser, compiler, matcher, limits and data tables in `src/RecursiveAI.OneEcma/RegExp/`. A port reproduces the behaviour from this page, ECMA-262 (17th edition) §22.2 and Annex B.1.2, and the V8 wording table at the end. The engine never uses a host regular-expression library. It implements the pattern grammar and semantics of ECMA-262 22.2 (flags `d g i m s u v y`, lookbehind, named groups including repeated names in different alternatives, modifier groups `(?ims-ims:…)`, the `v` flag with set notation and properties of strings, Unicode property escapes) plus the legacy grammar of Annex B.1.2 for patterns without `u`/`v`. The RegExp constructor, `lastIndex`, `exec` result objects and the `String.prototype` methods are the built-in's job and sit on top of this API. ## API - `RegExpProgram.Compile(pattern, flags)` returns a program or diagnostics. It never throws for bad patterns or flags. - `program.Exec(input, startIndex, sticky, fuel, limits)` returns `NoMatch`, `Match` (with `RegExpMatch`: start and end per group in UTF-16 code units, `-1` for an unmatched group) or `LimitExceeded` with the kind (`Fuel`, `Memory`). The `d` flag needs nothing extra: the indices are always there. `sticky` is the `y` flag; the caller owns `g` and `lastIndex`. - `program.GroupNames` lists named groups in order of first appearance; a repeated name maps to several group numbers and the caller reports the one that participated. When several matched in one result (only reachable through the nested form below) the highest-numbered one wins. - `RegExpValidator.Validate(body, flags)` and `ValidateLiteral(body, flags, literalStart)` return diagnostics only. They are the early-error hook for a parser that finds a regular-expression literal. - A start index inside a surrogate pair of a `u`/`v` pattern moves back to the pair's first half. - `Exec` allocates scratch per program and reuses it between calls; programs are immutable and safe to share across threads. ### Diagnostics A `RegExpDiagnostic` is `(Start, Length, Reason, Message, InFlags)`. `Start`/`Length` are zero-based UTF-16 offsets into the pattern (or into the flags string when `InFlags`). The span is the offending token: the `(` of an unterminated group, the quantifier of "Nothing to repeat", the name of a duplicate group, the range of "Range out of order in character class". One diagnostic is reported per pattern: the parser stops at the first error, as V8 does, and which error comes first follows V8's order (see below). `Message` is the complete V8 text `Invalid regular expression: //: ` with the flags in the fixed order `dgimsuvy`. Flag errors read `Invalid flags supplied to RegExp constructor ''`; through the literal hook they read `Invalid regular expression flags`. ## Semantics notes - Case: without `u`/`v`, Canonicalize is the toUppercase mapping restricted to single code units that do not map a non-ASCII unit to ASCII; with `u`/`v`, simple case folding (`CaseFolding.txt` statuses C and S). With `v` and `i`, class contents are simple-case-folded as the spec's set operations require. Sets are kept as sorted code-point ranges; with `i` (not `v`) the range set is augmented with canonical images and the input is canonicalized at match time. - Lookarounds are atomic: backtracking into a successful positive lookaround is impossible; captures set inside a positive lookaround stay, those inside a negative one are discarded. - A quantified group clears the captures it contains at the start of every iteration; an iteration that matches the empty string after the minimum is met fails (RepeatMatcher step 2.b). - Lookbehind matches backwards: terms evaluate right to left, backreferences read backwards. - Repeated group names: ECMA-262 22.2.1.1 forbids two groups of one name unless they sit in different alternatives of one disjunction (MightBothParticipate). The parser registers a named group when it closes, so the error appears at the second group of the pair, before any later error in the pattern. ## Limits | Limit | Value | Outcome | |---|---|---| | Fuel | the `FuelMeter` the caller passes; one unit per instruction step, per backtrack and per character consumed by a repeated single-character atom | `LimitExceeded`/`Fuel` | | Backtrack stack | `RegExpLimits.MaxBacktrackBytes`, default 64 MiB; entries are 16 bytes | `LimitExceeded`/`Memory` | | Pattern nesting (groups, lookarounds, nested classes under `v`) | 200 | diagnostic `Regular expression too large` | The matcher draws fuel from the meter in chunks of 1024 units (`FuelMeter.ChunkSize`), counts down locally and refunds what it did not use; a host that adds wall-clock or cancellation polling derives from `FuelMeter` and overrides `TryConsume`. A meter that refuses means the match call returns `LimitExceeded`; no exception leaves `Exec`. The matcher and the parser use no recursion that follows pattern or input size: the parser checks the nesting counter, the matcher keeps an explicit backtrack stack. ## Compiled program Programs are arrays of `int`. Each instruction is an opcode followed by operands; `fl` is a flag word (bit 1 backwards, bit 2 ignore-case, bit 4 dot-all). Jump targets are absolute indices. | Opcode | Operands | Meaning | |---|---|---| | `Char` 1 | cp, fl | one character (canonicalized when ignore-case) | | `Any` 2 | fl | `.` | | `Set` 3 | class index, fl | character class | | `Bol` 4 / `Eol` 5 | multiline | `^` / `$` | | `WordBoundary` 6 | negate, class index | `\b` / `\B` using the word class compiled for the active flags | | `Split` 7 | target | push a choice to `target`, continue | | `Jmp` 8 | target | jump | | `Mark` 9 | register | store the position in a register | | `EndGroup` 10 | group, register, backwards | set capture (start from the register, end at the position) | | `Clear` 11 | first group, last group | unset captures | | `Backref` 12 | backref table index, fl | backreference (several group numbers for repeated names) | | `LookStart` 13 | negate, resume target | begin a lookaround; pushes a barrier entry | | `LookEnd` 14 | negate | end it: discard choices (positive), or fail the lookaround (negative) | | `SetReg` 15 | register, value | | | `LoopSplit` 16 | counter register, min, max, greedy, exit target | counted loop head | | `LoopEnd` 17 | counter register, position register or -1, min, loop head | counted loop tail with the empty-iteration check | | `EmptyCheck` 18 | register | fail if the position did not move | | `Repeat` 19 | kind (0 char, 1 any, 2 set), argument, fl, min, max, greedy | single-character loop in one backtrack entry | | `Strings` 20 | trie index, fl | longest-first match over a `v`-mode string set | | `Match` 21 | — | success | The backtrack stack holds 4-int entries (kind, pc or register, position, extra): choice points, register-undo records and lookaround barriers. ## Unicode data `tools/RegExpUnicodeGen` reads the files pinned in `tools/RegExpUnicodeGen/pin.txt` (UCD 17.0.0 with SHA-256 each, plus the emoji sequence files) and writes `Data/regexp-unicode.bin`, embedded as a manifest resource and decoded lazily at first use. The output is deterministic. Layout, all integers as LEB128 varints: 1. magic `OERU`, version byte `1`; 2. property sets: count, then per set a kind byte (General_Category, Script, Script_Extensions, binary), the names (canonical and aliases, UTF-8 with length), and a range list (count, then pairs of gap-from-previous-end and length-minus-one); 3. simple case folds: count, then pairs (key gap, zigzag value delta); 4. non-Unicode uppercase pairs, same encoding; 5. string sets for `\p{Emoji_Keycap_Sequence}`, `RGI_Emoji_Flag_Sequence`, `RGI_Emoji_Modifier_Sequence`, `RGI_Emoji_Tag_Sequence`, `RGI_Emoji_ZWJ_Sequence`, `Basic_Emoji`: name, single code point ranges, then sequences (length, code points) sorted by code points. `RGI_Emoji` is their union, built at run time. Property names and values are exactly those of the supported-property tables in ECMA-262 22.2.2.9 (so `space` and `Script=Grek` work and `WSpace` does not). The file is about 95 KB. Regenerate with `dotnet run --project tools/RegExpUnicodeGen -c Release -- src/RecursiveAI.OneEcma/RegExp/Data/regexp-unicode.bin`. ## Deviations from Node (V8) The differential in `testdata/regexp/` (835 hand-written cases) and three seeded random sets of 15,000 patterns each, recorded with Node v24.22.1, agree on match results and error messages except for these, which the harness counts as documented: 1. **Match starting inside a surrogate pair.** In a `u`/`v` pattern V8 can return a match whose start index lies between the two halves of a pair (`/\B/u` on a string with astral characters, and lookaheads such as `(?!.)`). The specification only starts on code point boundaries; so do we. 2. **Modifier `i` leaks into `\W`, `\b`, `\B`.** After a group `(?i:…)`, V8 evaluates a later `\W`, `\b` or `\B` of a `u`/`v` pattern without `i` as if `i` were set, so U+017F and U+212A count as word characters. We apply the flags of the enclosing scope, as the spec does. 3. **Repeated group names.** For some arrangements of alternatives V8 accepts names that 22.2.1.1 rejects, for example `(x|y)|(?)(?1|)`, which depends on an earlier capturing group containing `|`. V8's rule is not a function of the spec's, so we implement the spec's MightBothParticipate with V8's registration time. Where V8 accepts `(?a(?b)|c)` (the inner group in a non-last alternative of the outer group's own body) and rejects `(?a|c(?b))`, we agree. 4. **Single characters in `\q{…}` under `v` and `i`.** V8 does not match `[\q{c}]` against `C` with flags `iv`, although `[c]` does; ECMA-262 22.2.2.7.3 folds both sides, so `[\q{c}]` matches `C` here. ## V8 message table Reasons the parser produces, each checked against Node v24.22.1 output in the 436 `error` cases of `testdata/regexp/expected.json` (the recorded file is the evidence: pattern, flags and Node's message): `Unterminated group`, `Unmatched ')'`, `Nothing to repeat`, `Lone quantifier brackets`, `Incomplete quantifier`, `Invalid quantifier`, `numbers out of order in {} quantifier`, `\ at end of pattern`, `Invalid escape`, `Invalid Unicode escape`, `Invalid decimal escape`, `Invalid named reference`, `Invalid named capture referenced`, `Invalid capture group name`, `Duplicate capture group name`, `Invalid group`, `Invalid flag group`, `Repeated flag in flag group`, `Unterminated character class`, `Range out of order in character class`, `Invalid character class`, `Invalid character in character class`, `Invalid set operation in character class`, `Negated character class may contain strings`, `Invalid property name`, `Invalid property name in character class`, `Regular expression too large`. Ordering rules observed: errors come in source order of the point where the parser first cannot continue; a repeated group name is reported at the closing of the second group. In `v` mode a `-` straight after a completed range (`[a-z-]`) is `Invalid character class`, while a stray `-` elsewhere is `Invalid character in character class`. ## Checks - test262 data translators: `built-ins/RegExp/property-escapes/generated` (469 files), `unicodeSets/generated` (114), `CharacterClassEscapes` (12) — the generated strings are matched by our engine exactly as `harness/regExpUtils.js` does; all pass. - Literal validation: every regular-expression literal in `language/literals/regexp`, `built-ins/RegExp/{property-escapes,named-groups,lookBehind,unicodeSets,match-indices}` and `annexB/built-ins/RegExp` through the validator. 13 files in `language/literals/regexp` are lexical (line terminators inside a literal, a `/` inside a body, escaped flags) and belong to the lexer. - Fuzzing: `fuzz/OneEcma.Fuzz` (`regexp [seed] [corpus.json]`) generates and mutates patterns and runs them under small fuel and memory limits, checking that nothing throws, compile and validate agree, matches are in range and each case ends in bounded time. - Run `tests/OneEcma.Tests` for all of the above; set `ONEECMA_TEST262_ROOT` to a test262 checkout to include the translators. # Intl formatting core Normative description of the engine-internal services behind `Intl.NumberFormat`, `PluralRules`, `ListFormat`, `RelativeTimeFormat`, `DisplayNames`, `DateTimeFormat`, `DurationFormat`, time zone identifiers and the locale machinery, as implemented in `RecursiveAI.OneEcma.Intl`. The JavaScript constructors and option reading (property access, `ToString`/`ToBoolean`/`ToNumber` conversions, abrupt completions from user getters) sit on top of these services and are not part of them: every option arrives as a string, integer or boolean, and an invalid value raises `IntlException` (`Kind` is `Range` or `Type`, which map to `RangeError` and `TypeError`). Data comes from the embedded CLDR 48.0 tables (`docs/locale-data.md`). Supported locales: `en-US`, `en-GB`, `es` (and `es-US`), `fr`, `de`, `pt-BR`; Gregorian calendar and Latin digits only. The default locale is a parameter of every `Create`. ## Surface | Service | Entry points | |---|---| | `LocaleId` | `Parse`, `Canonicalize` (UTS 35 language/script/region/variant aliases, keyword type aliases, extension order), `Maximize`, `Minimize` | | `LocaleOptions` | `Apply(tag, options)`: ECMA-402 14.1.1 language/script/region replacement and the `ca`, `co`, `hc`, `kf`, `kn`, `nu` keyword options | | `LocaleResolver` | `CanonicalizeLocaleList`, `BestAvailableLocale`, `Match`, `Resolve` (ResolveLocale with extension keys), `SupportedLocales`, `ValidateLocaleMatcher`, `ValidateNumberingSystem` | | `NumberFormatService` | `Create`, `Format`, `FormatToParts`, `FormatRange`, `FormatRangeToParts`; resolved options as properties | | `PluralRulesService` | `Create`, `Select`, `SelectRange`, `PluralCategories` | | `ListFormatService` | `Create`, `Format`, `FormatToParts` | | `RelativeTimeFormatService` | `Create`, `Format`, `FormatToParts` | | `DisplayNamesService` | `Create`, `Of` | | `DateTimeFormatService` | `Create` (ECMA-402 11.1.2 CreateDateTimeFormat, including the `required`/`defaults` of `toLocaleString` and friends), `Format`, `FormatToParts`, `FormatRange`, `FormatRangeToParts`; resolved options as properties, `Pattern` | | `DurationFormatService` | `Create`, `Format`, `FormatToParts`; `Unit(i)` gives the resolved style and display of each unit; `DurationRecord.Create` is ToDurationRecord after property reading | | `TimeZoneData` | `Canonicalize` (primary identifier), `Identify` (identifier as asked, case-normalized), offset zone syntax | | `ITimeZoneSource` | `TryGetOffset(zoneId, epochMs)`: the host's zone rules | | `IntlEnumeration` | `SupportedValuesOf(key)` | | `DecimalNumber` | exact decimal value from a double, BigInt or numeric string; nine rounding modes, increments, significant/fraction digit priority | Numbers are exact: a double is converted to its shortest round-trip decimal, a BigInt or numeric string keeps all digits, and rounding works on the digit string, never on binary floating point. Options: `localeMatcher` accepts `lookup` and `best fit` (both use the lookup matcher) and anything else is a `RangeError`. `numberingSystem` must match the Unicode locale `type` production (groups of 3 to 8 alphanumerics joined by hyphens) or it is a `RangeError`; a well-formed system other than `latn` is unsupported and falls back to `latn`, as ECMA-402 9.2.7 allows. `unitDisplay`, `currencyDisplay` and `currencySign` are validated whatever the style. ## Differential against Node The corpus in `testdata/intl/*.jsonl` is recorded from Node v24.22.1 (ICU 78.3, CLDR 48.0, Unicode 17.0) by `tools/intl-diff/gen-corpus.js`: a deterministic grid of values, options and locales per service, one row per call, with formatToParts output compared part by part. `deviations.txt` lists the rows where we differ on purpose. | Service | Rows | Match | Documented | Unexplained | |---|---:|---:|---:|---:| | NumberFormat | 4566 | 4566 | 0 | 0 | | NumberFormat formatRange | 720 | 714 | 6 | 0 | | PluralRules | 784 | 777 | 7 | 0 | | ListFormat | 766 | 766 | 0 | 0 | | RelativeTimeFormat | 1122 | 1118 | 4 | 0 | | DisplayNames | 1505 | 1483 | 22 | 0 | | Locale (canonicalize, maximize, minimize, supported*) | 580 | 566 | 14 | 0 | | DateTimeFormat (format, formatToParts, resolvedOptions, option errors, time zones) | 12928 | 12792 | 136 | 0 | | DateTimeFormat formatRange / formatRangeToParts | 2600 | 2595 | 5 | 0 | | DurationFormat (parts, resolvedOptions, option and value errors) | 3030 | 3029 | 1 | 0 | Deviations, with the rule followed: - **Huge values (`1e21`)**: ICU overflows the plural operands, so PluralRules and the RelativeTimeFormat plural choice differ; the CLDR rules on the exact value are followed. - **DisplayNames code canonicalization**: ECMA-402 canonicalizes region and script codes and calendar aliases before lookup (`us` is `US`); Node returns its input spelling or no name. - **Range with a currency code**: Node shifts the source labels of the code prefix; the specification's shared/start/end labels are kept. - **Locale set**: `supportedLocalesOf`, the resolved locale and `supportedValuesOf` depend on the available-locale set (six locales here, all of CLDR in Node); `supportedValuesOf` lists only what the data set carries (calendar `gregory`, numbering system `latn`). - **Locale algorithms**: `minimize` of `und-Hant` and the base name of `en-u-va-posix` follow the ECMA-402 algorithms; ICU keeps the language / variant keyword. Date-time and duration deviations: - **U+202F.** The reference's `format()` prints U+0020 where its `formatToParts` keeps the narrow no-break space of the CLDR 48 patterns (`3:05 PM`); we keep the CLDR text in both. - **Time zone identifiers.** The reference resolves a `timeZone` to the CLDR primary identifier; ECMA-402 keeps the identifier as asked (case-normalized), and so do we (test262 `timezone-case-insensitive`, `timezone-not-canonicalized`, `canonicalize-utc-timezone`). `Canonicalize` gives the primary identifier for lookups. - **Hour with fractional seconds** (`hour` + `fractionalSecondDigits`, mostly with the K cycle): ICU drops, reorders or annotates fields in ways CLDR does not describe (133 rows); we compose the pattern from the CLDR `appendItems`. `es-US` with a long month and a 2-digit day joins with a hyphen in the reference (3 rows); CLDR has `de`. - **Ranges**: a reversed range with `dateStyle: full` in `en-GB` loses the weekday comma in the reference (4 rows); `de` month-only January to December separates with thin spaces there (1 row). - **DurationFormat** formats a `NaN` field as the empty string in the reference; ToIntegerIfIntegral makes it a `RangeError`, which we follow. Where the specification leaves room, Node's behaviour is followed: Spanish list conjunction `y`/`e` and `o`/`u` before a vowel sound, percent and compact-percent patterns, the range collapse heuristic, the approximately sign from the locale symbols, and the 1% epsilon for `numeric: auto` relative times. ## Date-time and duration formatting ### DateTimeFormat `DateTimeFormatService.Create(locales, options, defaultLocale, defaultTimeZone, zones, required = "any", defaults = "date")`. The instant is epoch milliseconds (a `Date` time value, ±8.64e15); the zone offset comes from an `ITimeZoneSource` passed in, never from the machine. Gregorian calendar and Latin digits; any other `calendar` or `numberingSystem` falls back to them (ECMA-402 9.2.7). - **Pattern choice.** The options become a UTS 35 skeleton (`EEE`, `y`, `MMM`, `d`, `h`/`H`/`K`/`k` by hour cycle, `m`, `s`, `S` per fractional digit, `B` for `dayPeriod`, `z`/`zzzz`/`O`/`OOOO`/`v`/`vvvv` for `timeZoneName`), and `DateTimePatternGenerator` picks the CLDR pattern: the candidates are the single-field canonical patterns, the locale's date and time styles and `availableFormats`; the distance counts 0x1000 for a field the candidate lacks, 0x10000 for one it adds and the difference of the field types otherwise; a 12-hour candidate never stands in for a 24-hour request. A request no candidate satisfies is split into a date part and a time part, joined with `dateTimeFormats` (the `atTime` variant for formats; the plain one between a date and a time interval), and fields still missing are added with `appendItems`. Widths are then adjusted to the request. Ties go to the earlier candidate unless the later one lacks a higher-numbered field; candidates are visited in a fixed order, never by hash order. - **Styles.** `dateStyle`/`timeStyle` use the locale's `dateFormats`/`timeFormats` directly, glued with the `atTime` pattern of the date style. Mixing them with component options is a `TypeError` (`Invalid option : option`). - **Hour cycle.** `hour12` wins over `hourCycle`, which wins over the `hc` locale keyword, which wins over the locale default; the keyword is dropped from the resolved locale when `hour12` is given. `dayPeriod` is ignored when a 23- or 24-hour clock shows an hour. `fractionalSecondDigits` adds a seconds field when a minute is shown without one. - **Resolved options** are read back from the chosen pattern text (a quoted `de` therefore does not count as `day`), as the reference does. - **Time zones.** `Identify` returns the identifier as asked in the spelling of the time zone database (`Asia/Kolkata` stays `Asia/Kolkata`; `Etc/UTC`, `Etc/GMT` and `GMT` are kept; `UTC` is `UTC`; matching ignores ASCII case); an offset such as `+0530` becomes `+05:30`. Offsets and names are looked up under the CLDR primary identifier (`Asia/Calcutta`). Names: specific (`CEST`), generic (`CET`/`Central European Time`), `shortOffset`/`longOffset` (`GMT+2`, `GMT+02:00`, zero as `GMT`), with the locale's `hourFormat` and `gmtFormat`; the metazone in force (1970-01-01 up to 9999-12-31T23:00Z) decides, the daylight flag picks the specific name, a generic name needs the offsets half a year either side (a zone that does not change is generic in both). A zone without a metazone name gets its location name (the country name for a country's primary zone, else `City Time`), and the localized GMT format as the last resort. - **Parts** are `weekday`, `era`, `year`, `month`, `day`, `dayPeriod`, `hour`, `minute`, `second`, `fractionalSecond`, `timeZoneName` and `literal` (`relatedYear` and `yearName` need another calendar). `format` keeps U+202F where the CLDR pattern has it. - **Ranges.** `FormatRangeToParts` compares the two instants in their zone from the era down and takes the largest differing field. If the format shows nothing that differs, the date is printed once and every part is `shared`. A range that differs in a field the format lacks is widened first: a day-level range of a weekday with a time gets the day, a month-level one the month, a year-level one the year (a weekday alone, or a month alone, is not widened). The interval pattern for the skeleton and the differing field (`intervalFormats`) is used when the locale has one, its day-period, zone and numeric widths adjusted to the format; the text up to the first repeated field letter belongs to `startRange`, the rest to `endRange`, and text outside the fields spanned by both halves is `shared`. A date with a time that differs only in the time is the date once, joined with the plain date-time pattern, and the time as an interval. Otherwise both dates are printed in full around `intervalFormatFallback` (`{0} – {1}`), with literal text before the first and after the last field marked `shared`. A bare long month (`LLLL`) has no interval pattern and takes this fallback. A reversed range is formatted as given. - **Limits.** Parsing patterns and building parts are loops over the pattern and skeleton; widening a range builds one more service, once (the widened format has the fields it lacked). No recursion follows input. ### DurationFormat `DurationFormatService.Create(locales, options, defaultLocale)` follows ECMA-402 section 13: `style` (`long`, `short`, `narrow`, `digital`) and per-unit `style`/`display` options (`GetDurationUnitOptions`, `ValidateDurationUnitStyle`), `fractionalDigits` 0 to 9, `numberingSystem` (only `latn` is carried). Digital style and numeric units join hours, minutes and seconds with the separators of the `durationUnit` patterns (`:` in all six locales); sub-second units shown as `numeric` fold into the seconds as a fraction truncated to `fractionalDigits` (default up to 9). The unit pieces are formatted with `NumberFormatService` (style `unit`, `unitDisplay` from the unit style) and joined with the `unit` list pattern of `ListFormatService` (`digital` uses `short`). `DurationRecord.Create` takes the ten fields (null for undefined) and enforces integers, equal signs, the 2^32 limit on years, months and weeks and the 2^53 s limit on the total (exact arithmetic). One behavior the specification text does not state but the reference shows, and the corpus confirms: seconds shown as a number after a spelled-out unit (`5 min:07`) are appended to the previous list item with the minute/second separator instead of being a list item of their own. Parts carry `unit` for everything inside a unit's number and label; the list separators have none. ### Time zone source `ITimeZoneSource.TryGetOffset(zoneId, epochMs, out TimeZoneOffset)` answers the offset in milliseconds and whether daylight saving is in force, for a CLDR primary identifier; an unknown zone gives a `RangeError`. The services never read the operating system: the host supplies the rules (a tzdata reader, a recorded table in tests). `BclTimeZoneSource` wraps `TimeZoneInfo` for tests and for hosts that want it; instants outside the years 1 to 9999 use the offset at the nearest end. ## test262 `intl402` replay `tools/intl-test262/translate.js` runs the data-style tests of `test/intl402` (NumberFormat, PluralRules, ListFormat, RelativeTimeFormat, DisplayNames, Locale, Intl, DateTimeFormat, DurationFormat; run it with `TZ=UTC`) in a vm context where the Intl constructors record their calls; every assertion that compares a call result with a literal becomes a check (`{service, locales, options, method, args, expected}`). `IntlTest262Tests` replays the checks against the services (set `ONEECMA_INTL_TEST262_CASES` to the translator output). Checks for locales outside the data set (`ja`, `zh`, `en-IN`, `pt-PT`, ...) and shapes that belong to the JS layer (user getters, non-string locale objects) are skipped. Tests that read the parts of a result with `part.type === ...` get primitive types (only the values are tagged), and the date-time `format` results are compared with U+202F read as U+0020 because the expected strings come from the reference's `format()`. At the pinned test262 commit: 1,133 files, 362 translated, 24,869 checks; **14,119 pass, 0 fail, 1,129 documented, 9,621 skipped**. By area, the date-time and duration part: DateTimeFormat 3,710 pass, 15 documented, 138 skipped; DurationFormat 320 pass, 83 documented; `Intl.supportedValuesOf` and `getCanonicalLocales` 761 pass, 248 documented. `testdata/intl/test262-expectations.txt` documents each documented check: the numbering-system and calendar tests need non-Latin digits and non-Gregorian calendars, and one roundingIncrement check needs integer conversion in the option layer. ## Known gaps - Numbering systems other than `latn`; calendars other than Gregorian. - Locales outside the six (the lookup falls back to the default locale). - `Intl.Locale` `getCalendars`, `getHourCycles` and `getNumberingSystems` list one entry, the keyword if given and otherwise the region's preference: CLDR carries one for each of the six locales and Node gives the same lists for them. `getCollations` lists only the collation types the Collator can use (the tailorings of the language: `trad` for `es`, `phonebk` for `de`); Node also lists the root types `emoji` and `eor`, which the Collator does not implement (see the text services notes). Outside the six locales the lists follow the default-locale fallback, so Node's longer lists for `ja`, `ar`, `th` and others differ by design. - `best fit` is the lookup matcher. - DateTimeFormat has no non-Gregorian calendars, no `era` beyond BC/AD, and `Temporal` objects are not accepted (the JS layer converts `Date`s to time values). - The JS-facing constructors and `Date` arithmetic are not part of these services. - ICU's quirks above are not reproduced. ## Service cache `String.prototype.localeCompare`, `Number.prototype.toLocaleString`, `BigInt.prototype.toLocaleString` and the three `Date.prototype.toLocale*String` methods read their options exactly as the constructors do (same order, same getters), then look the resulting service up in a per-realm cache keyed by the resolved locale list, the host's default locale and zone, and the option record. A miss builds the service; the cache holds at most 64 entries and is emptied when full. Constructors (`new Intl.NumberFormat(...)`) never share services. A service only reads its options, so reuse cannot be observed. ## Throughput Date-time and duration (same method, 1000 random instants between 2020 and 2033, fixed-offset zone source): `format` of a numeric date and time 600 ns and 1 KB; full date with long time and a zone name in `de` 5.7 µs (the zone name probes the offsets half a year either side); `formatRange` of a date 2.8 µs; `DurationFormat.format` of a five-unit duration in `en` 3.3 µs and 14 KB (a unit-style NumberFormat per unit is cached, the list and parts are built per call). Release build on a shared 16-core Ryzen 9 7950X3D host, load about 5. Number formatting, as measured for the first services: Informal (`ONEECMA_INTL_THROUGHPUT=`, Release build, 1000 random doubles with 0 to 8 integer digits and up to 3 decimals, one thread, loaded shared host): `format` of a double 450 to 1,650 ns for decimal, currency and compact patterns (about 1.1 KB allocated each), `format` of a pre-converted decimal 360 to 610 ns, `PluralRules.select` 113 ns and 137 B. No recursion follows input size: rounding, grouping and pattern assembly are loops over the digit string. ## JavaScript surface `Builtins/Intl*.cs` exposes the services above as the ECMA-402 objects: the `Intl` namespace (`getCanonicalLocales`, `supportedValuesOf`), every constructor (`Locale`, `Collator`, `NumberFormat`, `PluralRules`, `ListFormat`, `RelativeTimeFormat`, `DisplayNames`, `DateTimeFormat`, `DurationFormat`, `Segmenter`) with `supportedLocalesOf`, `resolvedOptions`, the bound `format`/`compare` getters and the legacy constructor chaining for `NumberFormat` and `DateTimeFormat`, and the locale-sensitive overrides of `String.prototype.localeCompare`/`toLocaleUpperCase`/`toLocaleLowerCase` and `Number`, `BigInt`, `Array` and `Date` `toLocale*String`. - Options are read in the order ECMA-402 specifies (GetOption, GetNumberOption), so getters observe the same sequence as in the specification. - The default locale is `IJsHost.DefaultLocale`; the default time zone is `IJsHost.TimeZoneId`, with offsets from `IJsHost.TryGetTimeZoneOffset`. - `Date.prototype.toLocale*String` and `DateTimeFormat.prototype.format` keep U+202F from the CLDR patterns, as the intl402 tests expect; the reference engine prints U+0020 there. - test262 `intl402` on the engine: 1104 pass, 0 fail, 17 documented (non-Gregorian calendars, non-Latin numbering systems, one collation and one locale not in the data), 78 skipped for locales outside the supported set. - A JavaScript-level differential (`tools/intl-diff/js-surface.js`, 228 rows recorded from the reference engine) has 7 documented differences, listed in `tools/intl-diff/js-surface.deviations.txt`. # Locale data Normative description of the data the Intl implementation reads: where it comes from, the embedded format, and how to add a locale. A port must be able to reproduce the format from this text. ## Sources and pins | Input | Version | Pinned by | |---|---|---| | CLDR JSON (`cldr-48.0.0-json-full.zip`) | 48.0 | SHA-256 in `tools/locale-data/pin.txt` | | CLDR common (`cldr-common-48.zip`: `uca/allkeys_CLDR.txt`, `collation/*.xml`) | 48 | SHA-256 and the official SHA-512 in `pin.txt` | | tzdata (`tzdata2025b.tar.gz`, only `zone.tab`) | 2025b | SHA-256 in `pin.txt` | | UCD files (grapheme, word, sentence break properties, `emoji-data.txt`, `DerivedCoreProperties.txt`, `PropList.txt`, `Blocks.txt`, `DerivedGeneralCategory.txt`) | 17.0.0 | SHA-256 in `pin.txt` | CLDR 48.0 and Unicode 17.0 are what the reference Node releases (v22.22.1 and v24.22.1) report as `process.versions.cldr` / `process.versions.unicode`, so Intl output compared against them carries no data-version noise. Raw inputs are fetched by `tools/locale-data/fetch.sh` (checksum-verified, never committed). The licence for all of it is the Unicode License v3 (see `THIRD-PARTY-NOTICES.md`). ## Regenerating ``` tools/locale-data/fetch.sh # -> $ONEECMA_LOCALE_DATA or ~/oneecma/locale-data dotnet run --project tools/LocaleDataGen -- \ --cldr /cldr/48.0 --ucd /ucd/17.0.0 --tzdata \ --out src/RecursiveAI.OneEcma/Intl/Data/Generated ``` The output is a pure function of the pinned inputs: two runs give identical bytes (checked with `diff -r`). The `.bin` files are committed and embedded in the engine assembly as manifest resources named `oneecma-data/`. ## Files | File | Content | Bytes | |---|---|---:| | `supplemental.bin` | tree (collation tailoring rule text also covers the root `search` collation): likely subtags, aliases, parent locales, default content, currency data (digits, regions), week data, time data, calendar preferences, numbering systems, metazone mapping, plural ranges, day period rules, territory containment, right-to-left scripts, BCP 47 keyword data (calendar, collation, currency, measure, number, timezone including IANA aliases), collation tailoring rule text, list of stored locales | 373,342 | | `locale-.bin` | tree per stored locale, delta against its parent | und 31,520; en 148,874; en-001 15,736; en-GB 4,192; es 125,352; es-419 17,439; es-US 13,955; fr 133,642; de 130,050; pt 125,568 | | `plurals.bin` | cardinal and ordinal plural rule ASTs for every CLDR language | 6,968 | | `collation.bin` | CLDR root collation table, contractions, implicit-weight ranges, decimal digit zeros | 282,838 | | `breaks.bin` | UAX #29 property ranges | 17,607 | | `normalization.bin` | UAX #15 combining classes, one-step decomposition mappings, composition exclusions | 35,464 | Total 1,445,121 bytes. The largest parts are likely subtags (222 KB of the supplemental tree), the root collation table (283 KB), and per language the units (about 35 KB) and currency names (about 31 KB). `en-US` is CLDR default content of `en` and `pt-BR` of `pt`, so they have no blob of their own; `en-GB`, `es-US` store only their differences. ## Container All integers are little endian. A varint is unsigned LEB128, at most 5 bytes. Every blob starts with 8 bytes: `OEDT`, format version (1), kind (1 tree, 2 plurals, 3 collation, 4 breaks, 5 normalization), two zero bytes. A reader rejects any other version. ### Tree blobs (kind 1) After the 8 byte header, five u32: `stringCount`, `tableOffset`, `dataOffset`, `nodesOffset`, `rootOffset` (the last relative to `nodesOffset`; the others absolute). Then: - **String table** at `tableOffset`: `stringCount + 1` u32 offsets into the string data (the last is the data length). At `dataOffset`: the UTF-8 strings, sorted by byte order, so a reader binary-searches a key without decoding. Keys and values share the table. - **Nodes** from `nodesOffset`. A node starts with a varint `(count << 2) | tag`. Tag 2 is a string and `count` is its string id. Tag 0 is an object: `count` key ids (varints, ascending) then `count` child offsets (varints, relative to `nodesOffset`). Tag 1 is an array: `count` child offsets. Children precede their parent and identical subtrees are stored once, so offsets may be shared. - JSON numbers and booleans are stored as their text; JSON objects keep their keys, and properties that only carry prose (`_description`, `_since`, `_reason`, `_iso4217`, `_comment`, `_references`) are dropped. **Inheritance.** A locale blob holds only members whose value differs from its parent's resolved data (CLDR JSON `full` packages are already resolved against the locale's own parents, so the generator diffs resolved against resolved). Each locale blob has a member `@parent` naming its parent locale (empty for `und`). Parents follow CLDR `parentLocales`, else drop the last subtag: en-GB → en-001 → en → und; es-US → es-419 → es → und. A lookup walks the chain from the most specific blob until some blob has the path. Arrays and strings are replaced whole, never merged. The value `∅∅∅` is CLDR's explicit "nothing" marker and is stored as is. **Paths.** Locale trees have the members `numbers` (symbols, decimal/percent/scientific/currency/rational formats, compact patterns; Latin digits only), `currencies`, `gregorian` (months, days, quarters, day periods, eras, date/time/date-time formats, availableFormats, appendItems, intervalFormats), `dateFields` (fields and relative-time patterns), `timeZoneNames` (zone, metazone and region formats and names), `languages`, `scripts`, `territories`, `variants`, `displayNames` (keys, types such as calendars, code patterns, locale display pattern), `listPatterns`, `layout`, `units` (all CLDR units in long/short/narrow plus duration unit patterns, which also serve DurationFormat). The members below each keep their CLDR JSON names and structure. ### Plurals (kind 2) Two sections, cardinal then ordinal. Each: varint language count, then per language (sorted): varint-length UTF-8 id, a byte rule count, and per rule a byte category (0 zero, 1 one, 2 two, 3 few, 4 many, 5 other; `other` is implied and never stored), a varint AST byte length and the AST. The AST of a rule (UTS #35 Part 3): varint `or` count; per `or` term a varint `and` count; per relation a byte operand (0 n, 1 i, 2 v, 3 w, 4 f, 5 t, 6 c, 7 e), a varint modulus (0 none), a byte negated flag (`!=`), a varint range count and per range varint low and high (equal for a single value). Rules are tried in category order. Only `=` and `!=` relations exist in CLDR 48; the generator fails on anything else. Samples are dropped. ### Collation (kind 3) The CLDR root collation (`allkeys_CLDR.txt`, UCA 17.0.0, default alternate handling non-ignorable). Layout: varint-length version string; implicit classes (varint count; each u16 base, byte shift, byte mode, varint origin); implicit runs (varint count; each varint gap since previous end, varint length-1, byte class; class 0 covers every code point not in a run); single-code-point entries (varint count; each varint code point delta, byte CE count, CEs); contractions (varint count; each byte key length, varint code points, byte CE count, CEs). A CE is varint primary, varint secondary, byte tertiary with bit 7 set for variable (`*`) elements. After the contractions come the decimal digits for numeric collation: varint count, then varint code point deltas of each digit zero (every general category Nd run is ten digits long, so only its zero is stored). A reader of an older blob that ends after the contractions has no digits. Implicit weights (UTS #10 §10.1.3, Table 16) for code points without an entry: `AAAA = base + (cp >> shift)` (shift 0 means no addition); `BBBB = ((mode 0 ? cp - origin : cp & 0x7FFF) | 0x8000)`; the pair is `[AAAA.0020.0002][BBBB.0000.0000]`. Classes: Tangut and Tangut Supplement (FB00), Tangut Components and Supplement (FB01), Nushu (FB02), Khitan Small Script (FB03) (assigned code points only), core Han = Unified_Ideograph in the CJK Unified Ideographs or CJK Compatibility Ideographs blocks (FB40 + cp>>15), other Unified_Ideograph (FB80 + cp>>15), everything else (FBC0 + cp>>15). The reader's `MatchLongest` expects NFD text and matches contiguous contractions only; discontiguous contraction matching, variable weighting options and the Collator options (`caseFirst`, `numeric`, `sensitivity`, ...) belong to the Collator (docs/text-services.md). **Tailoring.** The root table is the default collation of en, fr, de and pt. Spanish (`standard`) adds `&N<ñ<<<Ñ`. The rule text of every non-alternate, non-draft `` of en, es, fr, de and pt, and the `search` collation of the root (for example `es` `traditional`, `de` `phonebook`) is stored in `supplemental.bin` under `collationTailoring//` exactly as CLDR writes it (decomposed, comments removed). The Collator applies it with the UTS #35 rule syntax; the data layer does not. **Deviation.** CLDR orders Han in the root by radical-stroke (`[radical ...]` data in FractionalUCA). The shipped table has the UCA code point order for Han (the implicit weights above). The Node differential must exclude Han ordering until that is added. ### Normalization (kind 5) After the header: varint range count and per range varint gap since the previous end, varint length-1, byte canonical combining class (only non-zero classes; code points not listed have class 0); varint mapping count and per mapping: varint code point delta, byte flags (bit 0: compatibility mapping, bit 1: Full_Composition_Exclusion), byte length, varint code points. Mappings are the single-step values of `UnicodeData.txt` field 5 with the `` removed; Hangul syllables are left to the algorithm of Unicode section 3.12. The reader expands every mapping recursively, once, when it loads, and derives the primary composites: canonical mappings of two code points that are not composition-excluded. ### Breaks (kind 4) Varint property count; per property: UTF-8 name (`Grapheme_Cluster_Break`, `Word_Break`, `Sentence_Break`, `Extended_Pictographic`, `Indic_Conjunct_Break`), varint value count and the value names (index 0 is the default for unlisted code points), varint range count and per range: varint gap since the previous end, varint length-1, byte value index. Adjacent ranges with the same value are merged. ## Time zone data Date-time formatting needs the display names of time zones and the country of each zone. `supplemental.bin` carries, besides the CLDR metazone mapping: - `zoneRegions`: CLDR canonical zone id to the ISO 3166 region of the `zone.tab` line that names it or one of its aliases (tzdata 2025b, public domain; `fetch.sh` extracts `zone.tab` from the pinned archive). - `windowsZones`: IANA zone id to Windows zone id (every id in the type lists of CLDR 48 `windowsZones.json`; the first mapZone naming an id wins). A host on Windows without ICU only knows zones by Windows id, so the BCL time-zone source retries an unknown IANA id through this table (an alias goes through its canonical id first). An id that neither lookup resolves is unknown: Intl throws a RangeError and Date local time throws one too, never reading it as UTC. - `primaryZones`: region to the zone CLDR names as that region's primary zone (a zone whose location name is the bare country name). - `metaZoneInfo`: per zone, a list of periods `usesMetazone` with `_mzone` and optional `_from` and `_to` (`yyyy-MM-dd HH:mm`, UTC; a missing `_from` is 1970-01-01). - `metaZoneMap`: the reference ("golden") zone of each metazone per territory, `001` for the world. The data holds no offsets: the services ask an `ITimeZoneSource` for the UTC offset and daylight flag at an instant (see `docs/intl.md`). **Empty markers.** A child locale that blocks a name its parent has (CLDR spells this with the inheritance marker, e.g. en-GB short names that are absent from en-001 although present in en) is stored as an explicit empty string inside `timeZoneNames`, so that the delta against the parent does not make the parent's name reappear. A reader treats an empty string as "no name" and falls back to the localized GMT format. ## Adding a locale 1. Add its id (and any intermediate parent the CLDR chain needs) to `Locales` in `tools/LocaleDataGen/Program.cs`. The id must exist in the CLDR JSON `full` packages; region locales whose data is the language's default content need no entry. 2. For a Latin-script locale with a different collation, add its language to `CollationLanguages`; for plural data nothing is needed (all languages ship). 3. Add the tag to `CldrData.SupportedLocales`, run the generator, and add probes to `tests/OneEcma.Tests/LocaleDataTests.cs` with expected values from the reference Node. Another numbering system or calendar is a generator change: `DropNonLatinNumberSystems` and the `ca-*` sources. ## What is not in the data - Territory-to-time-zone lists (`Intl.Locale.prototype.getTimeZones`) need the tz database, which CLDR does not carry; the IANA zone names and aliases come from the BCP 47 timezone data (`_iana`, `_alias`), not from a tz release. - Language matching data (`languageMatching`), unit conversion and preference data, person names, transforms, annotations and non-Gregorian calendars. ## Reader `RecursiveAI.OneEcma.Intl.Data` (internal): `CldrData` (entry point; `GetLocale`, `TryResolveLocale`, `Supplemental`, `Plurals`, `Collation`, `Breaks`), `DataTree` (path lookups across the inheritance chain), `TreeBlob`, `PluralData` / `PluralOperands`, `CollationData`, `BreakData`, `NormalizationData`. Loading is lazy and cached; every object is immutable and thread-safe. No reflection other than reading the manifest resource. # Text services Normative description of the three Unicode text services the engine builds on: normalization (UAX #15), collation (UTS #10 with the CLDR root order and UTS #35 tailorings) and segmentation (UAX #29). They are internal C# services in `RecursiveAI.OneEcma.TextServices`; `String.prototype.normalize`, `Intl.Collator`, `Intl.Segmenter` and `localeCompare` wrap them. Data: Unicode 17.0.0 and CLDR 48.0 (see `locale-data.md` for the embedded formats). Nothing here uses the platform's globalization or normalization support. ## API (internal) | Type | Members | |---|---| | `Normalizer` | `Normalize(string, form)` (same instance back when already normalized), `Normalize(ReadOnlySpan, form)`, `TryNormalize(source, destination, form, out written)`, `IsNormalized(span, form)`, `Decompose(span, compatibility, CodePointBuffer)` (NFD or NFKD as code points, for collation). `NormalizationForm` = `Nfc`, `Nfd`, `Nfkc`, `Nfkd`. | | `TextSegmenter` | `Enumerate(span, granularity)` (a `foreach`-able ref struct yielding `TextSegment(Start, End, IsWordLike)`), `NextBoundary(span, start, granularity, out isWordLike)`, `TryFindSegment(span, index, granularity, ...)` (ECMA-402 `containing`). `SegmentGranularity` = `Grapheme`, `Word`, `Sentence`. | | `TextCollator` | `Create(locale, CollatorOptions?)`, `Compare(span, span)`, `GetSortKey(span)`, `TryGetSortKey(span, destination, out written)`. Instances are immutable and thread-safe. | | `CollatorOptions` | `Strength` (`Primary` to `Identical`), `CaseLevel`, `CaseFirst` (`Off`, `Lower`, `Upper`), `Numeric`, `IgnorePunctuation`, `Usage` (`Sort`, `Search`), `Collation` (BCP 47 `-u-co-` type such as `phonebk`, `trad`), `WithSensitivity("base" | "accent" | "case" | "variant")`. | Text is UTF-16; lone surrogates pass through as ordinary code points of no special class (normalization leaves them alone, collation gives them implicit weights, segmentation treats them as `Other`). Nothing depends on the thread's culture. ## Normalization NFC, NFD, NFKC and NFKD by the algorithm of UAX #15 section 3: full decomposition (table lookup, Hangul syllables by the arithmetic of Unicode 3.12), canonical ordering of each run of non-starters, and for the composing forms the canonical composition algorithm (D117) with the Hangul and primary-composite rules. `IsNormalized` uses the quick-check properties (derived from the same data: no for composition-excluded canonical mappings and for compatibility mappings under NFKC, maybe for characters that can compose backward and for composites whose decomposition starts with one, per UAX #15 section 9.2) and falls back to normalizing and comparing for "maybe". Pure ASCII input returns immediately. Time is linear in the input. The one exception is a single run of more than 32 combining marks, which is sorted in n log n; the sort is stable on (class, position). ## Collation ### Algorithm 1. The text is decomposed to NFD (the table is keyed on NFD). 2. Collation elements are looked up with the longest-match rule of UTS #10 section 3.3: the longest contiguous contraction (up to three code points, the root table's longest), then **discontiguous** extension (S2.1.2 and S2.1.3): a following non-starter that is not blocked from the match and forms a table entry with it joins it and is removed from its position. At most 64 following non-starters are examined for one match; real text has a few, and the bound keeps hostile runs of marks linear. Code points without an entry get the implicit weights of UTS #10 section 10.1.3 (Han, Tangut, Nushu, Khitan, unassigned). 3. Elements are converted to a working scale (primary x 4096, secondary and tertiary x 256) so that a tailoring can place a new weight between two neighbours. 4. A sort key is built level by level (UTS #10 S3): primaries (4 bytes each), then 0, secondaries (3 bytes), optionally the case level, tertiaries (2 bytes), the fourth level when punctuation is ignored and strength allows, and for `Identical` the NFD code points. Zero weights are omitted. `Compare` builds both keys and compares the bytes. ### Compare and sort keys Both are provided. `Compare` is the primary entry point. Sort keys exist because sorting n strings with `Compare` rebuilds 2 n log n keys; sorting by cached keys was about 30 times faster on 627 short strings (65 microseconds against 2.2 ms, see Throughput). Keys are plain byte strings whose byte order equals the collator's order (checked in the tests); they are only comparable with keys of an identically configured collator. ### Options (ECMA-402 section 10) | Option | Behaviour | |---|---| | `sensitivity` `base` / `accent` / `case` / `variant` | strength Primary / Secondary / Primary with case level / Tertiary. | | `ignorePunctuation` | Variable weighting "shifted" (UTS #10 section 4): elements marked variable (spaces and punctuation; the CLDR root's `maxVariable` is punctuation, so symbols and currency signs still count) drop out of levels 1 to 3, as do ignorables directly after them. | | `numeric` | A run of decimal digits (general category Nd, any script) compares by value; leading zeros do not count; digits of any length. Implemented as a marker element just below the primary of "0", then the digit count, then base-100 pairs. | | `caseFirst` `lower` / `upper` | Case is ordered ahead of the rest of the tertiary weight. Case classes are lower, mixed, upper (mixed is a contraction written with both cases, "Ch"). Case follows the tertiary weight: the capital forms 8 to 0xC, squared capitals 0x1D and normal-size kana 0xE, 0x11, 0x12 (small kana count as lowercase), and for tailored elements the letters they are written with. The default `Off` uses the table's raw tertiary order. | | `usage: "search"` | The CLDR `search` collation of the locale (root search rules plus the locale's own): equates, for example, compatibility Arabic forms and Korean jamo, and does not apply the Thai/Lao prevowel contractions. | | `-u-co-` type | `phonebk` (German) and `trad` (Spanish) select the tailoring of that name; a type the locale lacks is ignored. | ### Tailorings `CollationRules` parses UTS #35 collation rule syntax and applies it to a copy of the root table: `&` resets (a string, or `[last primary ignorable]`), the relations `<`, `<<`, `<<<`, `=`, the shorthands `;` and `,`, quoting (`'...'`, `''`) and backslash escapes, `[import tag-u-co-type]`, `[suppressContractions [set]]`, and a number of settings that do not affect Latin text (`normalization`, `strength`, `caseFirst`, ...) which are read and ignored. A new weight is the next unit above its reference at the relation's level, with the lower levels set to the common weights. Syntax the supported locales do not use (`[before n]`, `/` expansions, `|` prefixes, `<*` lists, `<<<<`, `[reorder]`) raises `NotSupportedException`, never a silently wrong order. The rule text of the supported locales (es standard, traditional and search; de phonebook and search; root search) is stored in the supplemental data; en, fr and pt use the root order. Every stored rule set parses (a test creates each). ### Known gaps - **Han** orders by code point (implicit weights), not by radical and stroke as the CLDR root does. The Node differential and the conformance files do not exercise it. - Backwards secondary weights (fr-CA), `[reorder]` of scripts, and the draft `eor` and Han-specific collations are not supported. The six locales do not need them. - A tailoring whose reference adds more than 4095 primaries (or 255 secondaries or tertiaries) between two root weights would collide; the stored rules add at most a few. - `maxVariable` is fixed at punctuation. ## Segmentation UAX #29 default boundaries, extended grapheme clusters (rules GB1 to GB999 including GB9c, the Indic conjunct rule, GB11 emoji ZWJ sequences and GB12/13 regional-indicator pairs), word boundaries (WB1 to WB999) and sentence boundaries (SB1 to SB998). Each boundary decision reads forward from the segment start with a few fields of state (the last two classes, the regional-indicator count, the sentence-terminator state); the two unbounded rules are linear in total: WB4 skipping (each run is walked a constant number of times) and the SB8 look-ahead (it stops at the next sentence terminator, so each character is examined once per preceding terminator it follows, and a terminator ends the previous look-ahead). `IsWordLike` for words follows ECMA-402 and what the reference implementation reports: true when the segment contains a letter, a number, katakana, hiragana or a Han ideograph, or two or more connector punctuation characters in a row ("__" is word-like, "_" is not). Hiragana and Han are recognized by code point ranges because UAX #29 gives them no word class. ### Locale handling and known gaps - The reference Node applies no locale tailoring to Intl.Segmenter for the six locales: the corpus below segments identically in all seven tags, and sentence breaks after abbreviations ("Mr. Smith") are **not** suppressed. This implementation does the same, so there are no suppression lists. If an application wants CLDR's `ss=standard` behaviour it would be an added option, not the default. - **Dictionary segmentation** (Han, Thai, Lao, Khmer, Burmese, and Japanese words) is not implemented: the default rules break between ideographs and hiragana and treat Thai letters as no class, so each cluster stands alone. Node uses an ICU dictionary there. Documented differences are listed in `tests/OneEcma.Tests/SegmenterTests.cs` with the reason. - `TryFindSegment` scans from the start of the text (linear in the index). ## Conformance and differential results Files are fetched by `tools/locale-data/fetch.sh` (pinned SHA-256 in `tools/locale-data/pin.txt`) and never committed; the tests skip with a message when they are absent. | Test file | Version | Result | |---|---|---| | `NormalizationTest.txt` | 17.0.0 | 20,034 of 20,034 lines for NFC, NFD, NFKC, NFKD, the is-normalized check on every column, and every code point outside Part 1 unchanged | | `GraphemeBreakTest.txt` | 17.0.0 | 766 of 766 | | `WordBreakTest.txt` | 17.0.0 | 1,944 of 1,944 | | `SentenceBreakTest.txt` | 17.0.0 | 512 of 512 | | `CollationTest_CLDR_NON_IGNORABLE.txt` | CLDR 48 (`common/uca`, UCA 17.0.0 root) | 206,298 of 206,298 lines in order at identical strength | | `CollationTest_CLDR_SHIFTED.txt` | CLDR 48 | 227,809 of 227,809 lines in order (shifted, identical strength) | Node differential (`tools/text-diff/`, expectations recorded in `testdata/text/`; Node v22.22.1 with ICU 78.2 and the reference Node v24.22.1 with ICU 78.3 produce byte-identical expectation files, both CLDR 48.0 and Unicode 17.0): - Collation: 627 strings in six languages, symbols, numbers, kana, combining sequences and other scripts (no Han) over 768 combinations of 9 locale tags (`en-US`, `en-GB`, `es`, `es-US`, `fr`, `de`, `pt-BR`, `de-u-co-phonebk`, `es-u-co-trad`), four sensitivities, ignorePunctuation, numeric, three caseFirst values and both usages: **0 mismatches**. - Segmentation: 53 strings at three granularities, all seven tags agreeing with each other: matches except three documented dictionary cases (Thai, Han, Japanese words). To refresh the expectations: `node tools/text-diff/gen-collation.js > testdata/text/collation-expected.json` and `node tools/text-diff/gen-segmentation.js > testdata/text/segmentation-expected.json`, with the corpus from `tools/text-diff/make-collation-corpus.py` and `testdata/text/segmentation-corpus.json`. ## Throughput Informal, one core of a 32-thread host (load average about 2.6), .NET 10.0.11, Release, `dotnet run -c Release` of the test project with `ONEECMA_TEXT_THROUGHPUT=1`. The mixed text is 4 million characters of the segmentation corpus (mostly Latin, with accents, emoji and several scripts); the ASCII text is 4.5 million characters of English. | Service | Result | |---|---| | NFC / NFD / NFKC / NFKD, mixed text | 121 / 260 / 49 / 245 million chars/s | | NFC of ASCII (quick check) / NFD of ASCII | 2,100 / 1,070 million chars/s | | Grapheme / word / sentence segmentation | 36 / 53 / 75 million chars/s | | Collation `Compare`, default / numeric + shifted | 3.2 / 2.3 million compares/s (short strings) | | Sort keys | 5.7 million keys/s | | Sorting 627 strings | 65 microseconds by cached keys, 2.2 ms by `Compare` | ## Safety No recursion follows the input. Every loop is bounded by the input length or by a small constant (contraction length 3, discontiguous scan 64). Buffers are pooled arrays sized by the input. A seeded random test runs all services over 4,000 and 1,500 strings of interesting code points (marks, Hangul, regional indicators, ZWJ sequences, lone surrogates, noncharacters) and checks idempotence, segment coverage, and that the collator is a consistent order (a sort with an inconsistent comparer throws). Pathological inputs (400,000 combining marks, 200,000 regional indicators, 200,000 "1. ", 300,000 digits in numeric mode) finish in the tests. # Syntax tree, scopes and parser behavior Normative description of what `JsParser.Parse` produces. A port must reproduce the tree shape, the scope information and the error behavior described here. ## Entry point `JsParser.Parse(source, options, cancellationToken)` returns `ParseResult { Program, Source, Diagnostics, HasErrors }`. It never throws for malformed input; every problem is a `Diagnostic`. `ParseOptions`: | Option | Default | Meaning | |---|---|---| | `Goal` | `Script` | `Script`, `Module` (always strict, top-level `await`) or `Eval` | | `Strict` | false | start in strict mode | | `FileName` | `` | name shown in reports | | `MaxDepth` | 512 | nesting limit; exceeding it reports `depth-limit` and stops | | `MaxErrors` | 100 | parsing stops after this many errors | | `AllowReturnOutsideFunction`, `AllowNewTarget`, `AllowSuperProperty`, `AllowSuperCall`, `InClassFieldInitializer`, `PrivateNames` | off | context for direct `eval` code and hosts that wrap source in a function body | | `RegExpValidator` | the engine's `RegExpValidator.ValidateLiteral` | `(body, flags, bodyStart) -> IReadOnlyList`: problems with source-offset spans, empty when valid; the first becomes the SyntaxError (headline = its V8 message, span = its token); null accepts all patterns | ## Positions Every node has `Start` and `End`, UTF-16 offsets into the source. `SourceText.GetPosition(offset)` gives the 1-based line and column; line terminators are LF, CR, CR LF, U+2028 and U+2029 (ECMA-262 12.3). The `-->` line of a report is the start of the primary span. ## Tree shape - Node classes live in `RecursiveAI.OneEcma.Syntax` (`Ast.cs` is generated by `tools/gen-ast.py` from a table in that script; edit the table, not the output). Every node has `Start`, `End` and `Kind`. Expressions add `IsParenthesized` (there is no parenthesized-expression node); statements and declarations derive from `Statement`. - One `FunctionNode` covers declarations, expressions, arrows, methods, getters/setters and generators/async variants, with flags (`IsArrow`, `IsAsync`, `IsGenerator`, `IsMethod`, ...), `Id`, `Params`, `Body`, `Scope`, and `UsesArguments`, `UsesThis`, `HasDirectEval`. One `ClassNode` covers class declarations and expressions. - Binding and assignment targets are separate pattern nodes (object, array, assignment, rest patterns), produced by reinterpreting the cover grammar (`a = [x, y]`, arrow parameters) with the early errors of ECMA-262 13.15.5 and 15.2.1. - Optional chains are a `ChainExpression` wrapper around member and call nodes (ECMA-262 13.3.9). - Module declarations (`import`, `export`) have their own nodes; `import(...)` is `ImportCall`, `import.meta` and `new.target` are `MetaProperty`. - Error recovery inserts `ErrorExpression` (empty span at the error) where an operand is missing; a tree with errors is for diagnostics, not for execution. - Operator chains are flat in source but nested in the tree, so `a+b+c+...` with 100k operands gives a tree 100k deep. Consumers must walk trees iteratively or check a depth counter. ## Scopes Scope analysis runs inside the parser (early errors need it) and leaves `Scope` objects on the tree: `Scope { Kind, Parent, Children, Bindings, ContainsDirectEval, ContainsWith, IsStrict, PrivateNames }` and `Binding { Name, Kind, Span, FunctionDeclaration }`. Kinds: global, module, eval, function, block, catch, for, switch, class, class field initializer and class static block. Redeclaration, `let`/`var` clashes, duplicate parameters, labels, private names, `break`/`continue` targets, `yield`/`await`/`arguments`/`super`/`new.target` context and strict-mode restrictions are reported as early errors in V8's wording (`syntax-messages.md`). Annex B: a function declaration in a block (sloppy mode) is hoisted to a `var` of the same name unless that would conflict (B.3.3). The declaration carries `IsAnnexBHoisted` and the function scope gets an `AnnexBFunctionVar` binding. Also accepted: HTML-like comments (B.1.1), legacy octal literals and escapes (sloppy only), `__proto__` duplicates rules, `for (var x = 1 in y)` (sloppy), labelled function declarations (sloppy), `if` function bodies. ## Error recovery and reporting - The parser reports every syntax error it finds. After an error it inserts the missing token or skips the stuck token (recorded in `Diagnostic.Recovery`) and resynchronizes at statement boundaries and at the closing bracket. - **Resynchronization:** an error inside a parameter list skips to the next `,` or the closing `)` on bracket structure; a broken `if`/`while`/`for`/`with` head skips to its `)` on the same line. The body is then parsed as a body instead of as fallout of the head. - **Cooldown:** a new error within 3 tokens of a syntax error, or within 12 tokens on the same line, is dropped as fallout; an end-of-input error is exempt from the same-line window (an unclosed bracket is always worth saying). An early error (not a grammar error) within two lines after a syntax error is dropped as well. After a lexical error (unterminated string, typographic quote) errors on the same and next line are dropped. Deviation: two real mistakes closer than that are reported as one. - **Hints:** after parsing, `SyntaxHints` recognizes common mistakes (Python `elif`/`def`/`and`/`or`/`for ... in range`, TypeScript annotations, `as` casts and `interface`, HTML entities, typographic quotes, stray closers, a bracket still open at the end of the input) and may move the primary span to the construct that is wrong and add a label and `help`. The headline is never changed. A hint never changes the source. - A regular expression literal's flags are validated by the lexer; the pattern body goes to `RegExpValidator`. ## Limits and hostile input - Recursion is bounded by `MaxDepth` plus `RuntimeHelpers.TryEnsureSufficientExecutionStack`; hitting either reports the `depth-limit` diagnostic ("Maximum nesting depth exceeded") and ends the parse. Tested with 100,000 nested `(`, `[`, `{`, functions, arrows, unary and ternary chains on a 256 KB thread stack. - Cancellation is polled every 256 tokens. - Work per error is bounded by a constant window of source text, so a long file full of errors stays linear. ## Documented deviations from Node - Sloppy-mode call expressions are accepted as assignment, update and for-in/of targets (web compatibility, Annex B test262 tests); they are a runtime `ReferenceError`. - Errors closer together than the cooldown are merged (above). - Where Node reports one error, the parser may report more after it; the first error's headline and line are always Node's (`SyntaxMessageTests`). # Executor shape: closure tree or bytecode > **Status.** The engine's executor is a *stack* bytecode machine with an explicit frame stack and the 16-byte `JsValue` > described here; it was built after this study and replaced the spike, which has been removed. Its speed > against Jint is measured by `bench/OneEcma.Benchmarks` (`EngineBenchmarks`). The text below is kept for the reasoning behind > the frame, depth, suspend and tracing decisions, which the engine follows; its timings are those of the study. Result of a measured spike (a throwaway study project). Two executors were built over the real syntax tree and benchmarked against each other and against Jint 4.17.0 on the same sources. ## Recommendation **Register bytecode with an explicit frame stack (shape B).** Speed does not decide it: both shapes are several times faster than Jint on every script except string building, so the "no worse than about 2x Jint" floor is met with a large margin (the closure tree is 1.0 to 1.6x faster than bytecode on call-heavy code; bytecode is 10 to 15x faster on throw/catch). What decides it is the other requirements: | Requirement | Closure tree (A) | Bytecode (B) | |---|---|---| | (a) no unbounded host-stack recursion | Every JavaScript call costs about 1 KB of host stack. Reachable depth was 854 calls on a 1 MB stack, 7,689 on 8 MB, 232,542 on 64 MB. The depth limit is therefore a property of the thread the host happens to call from, which breaks "the limit yields the same diagnostic every time". Deep expression trees recurse the same way and need a guard. | JavaScript calls do not touch the host stack: 400,000 deep on a 1 MB thread (the probe's cap). The limit is a counter and is identical on every host. Only native-to-script re-entry (a `sort` comparator) uses host stack, and that is counted. | | (b) generators, async, await | No cheap way. Needs a second, resumable form of every node, or a lowering to a flat form, which is a bytecode compiler anyway. | A suspended frame is a copy of its register window plus a pc (see "Suspend"). Natural. | | (c) fuel | A field decrement on loop back-edges and calls. | The same, in the back-edge jump instructions and in calls. No difference. | | (d) lean run with spans only, traced replay with the same semantics | Hooks mean a second tree built by a decorating factory, and fused nodes (a register compared with a constant) have no child nodes to hook, so the traced tree must disable the fusions: two code paths that must be kept identical. Frames are pooled, so locals vanish on return. | One dispatch loop generic over a tracer struct. The lean instantiation has empty inlined hooks. The pc maps to a span through a table. Frames and registers are heap data that stay intact until overwritten. | | (e) no IL, safe code, AOT | Fine. | Fine. The tracer generic is a struct type argument known at compile time, so NativeAOT compiles both instantiations (an AOT check app). | The cost of choosing B: a compiler with register allocation, jump patching and compile-time environment depth, a dispatch loop that must be kept correct by tests, and 1.0 to 1.6x slower steady-state calls than a closure tree. Nothing in the measurements suggests that cost matters for the floor. A hybrid (tree for plain functions, bytecode for generators) is rejected: it doubles the semantic surface the conformance suite has to cover. ## What was built All of it is BCL only, safe code, no IL emission. - Shared: `JsValue`, shapes and inline caches, arrays with a dense store, ropes for string building, a realm with intrinsics (`Math`, `String.fromCharCode`, a few array and string methods, `Error`), and an analysis pass that resolves identifiers to bindings, decides which bindings are captured and which can be read before initialisation. - (A) `Closure/`: each AST node becomes an object with `Eval`/`Exec`. Frames are pooled by call depth. Operator chains of more than 32 links evaluate with a loop. - (B) `Bytecode/`: 16-byte instructions `{op, a, b, c}`, a register machine over one shared register stack, a span table, a handler table for `try`, per-site inline caches. - Subset: numbers, strings, booleans, undefined, null; objects with prototype chains; dense arrays; functions, closures, arrows, `this`, `new` on constructor functions; `var`/`let`/`const` with TDZ and per-iteration `let` environments; `if`/`for`/`while`/`do`, `break`/`continue` (unlabelled); `throw` and `try`/`catch`. Everything else (classes, destructuring, spread, generators, async, `finally`, `switch`, labels, getters/setters, regular expressions, `with`, `eval`, `arguments`, template literals, optional chaining, `delete`) throws `SpikeUnsupportedException`. Deliberate deviations: top-level `var`/function declarations are locals of the program, not global-object properties; sloppy `this` is `undefined`; no Annex B function-in-block var copy; number formatting is approximate. Tests (48 cases): the benchmark scripts against values computed in C#; semantics checked on both executors (closure capture per iteration, TDZ, const assignment, error messages with the callee text taken from the span, evaluation order, `try` through frames); fuel limits on four loop shapes and on recursion; a 100,000-term sum and a 100,000-link member or call chain on a 256 KB thread; recursion depth on a thin thread; the traced run. ## Value representation `readonly struct JsValue { object? Ref; double Num; }`, 16 bytes. A number is `(NumberTag, d)`; undefined is `Ref == null` so zeroed registers and slot arrays are already correct; null, true, false and the TDZ marker are tag objects; strings are `string` or `Rope`; objects are `JsObject`. Numbers never allocate. Safe code only, no NaN boxing. Measured on the same arithmetic loop (5,000,000 iterations, four operations each; BenchmarkDotNet, in-process, one run, subject to the load noted below, ratios are robust to it): | representation | mean | vs raw `double` | allocated | |---|---:|---:|---:| | raw `double` (floor) | 6.1 ms | 1.0x | 60 B | | 16-byte struct (reference tag + double), the spike's choice | 44.5 ms | 7.3x | 768 B | | heap object per number (`sealed class` hierarchy) | 197.9 ms | 32.5x | 480 MB | | boxed `double` in an `object` slot | 121.4 ms | 19.9x | 480 MB | The struct is 3 to 4.5 times faster than either heap form and allocates nothing; the heap forms would need a small-integer cache and escape tricks to approach it, and still allocate for every non-integer result. Property keys are strings. Objects are a `Shape` (prototype included, so a shape hit implies the prototype) and a slot array; property sites cache `shape -> slot` for own properties, `shape + holder shape -> slot` one prototype up, and `shape -> shape` for adding a property. Attributes, accessors, deletion and dictionary mode are not in the spike; they belong in the shape (flags per key) and in a dictionary-mode fallback. ## Frame and environment layout - Analysis assigns every binding either a **register** of its function (when no nested function refers to it) or a slot in a heap **environment** (`Env { Parent, Slots }`) belonging to its scope. Only scopes with at least one captured binding materialise an environment; a closure captures the current `Env` pointer. Variable access is `(register)` or `(hops, slot)`, with hops counted at compile time over the scopes that have environments. - `for (let ...)` with captured bindings copies the environment each iteration before the update expression. - Bindings that can be read before initialisation (a reference earlier in the text, a reference inside the declarator's own initialiser, or any captured `let`/`const`/`class`) are filled with a TDZ marker on scope entry and checked on read; all others skip both. - Bytecode frames: one `JsValue[]` register stack. A call puts the callee, `this` and the arguments in consecutive temporaries; the callee's registers start at the first argument, so arguments are passed without copying. Registers beyond the arguments are cleared on entry. A `Fr` record (function, pc, base, env, this, return register) is pooled per depth and grows on demand. The stack is bounded (64 Mi slots) and checked before growth. ## Fuel, depth and cancellation - Fuel: `--fuel < 0` on every loop back-edge (the compiler emits distinct back-edge jump instructions, including a conditional jump that is always a back-edge) and on every call, script or native. Wall clock and cancellation are polled at the same places, every N charges, in the real engine. A `while (!done) {}` shape compiles to a back-edge too (tested). - JS call depth: bytecode compares the frame index with `MaxDepth` at the call instruction. Closure tree: the same counter, plus `RuntimeHelpers.TryEnsureSufficientExecutionStack` every 8th call as a host guard, which makes the effective limit depend on the host thread. - Compile-time recursion: the front end bounds nesting at 512 but accepts 100,000-link flat chains of binary operators, member accesses and calls (probe output below). Binary and logical chains are walked iteratively in both compilers. Member and call chains are not; every pass has a nesting counter (1,000) and a stack-space check that ends in a limit exception, tested at 100,000 links on a 256 KB thread. Without the stack-space check the counter alone overflowed at 169 levels in a debug build. - The interpreter loop and the closure tree both rely on the parser having bounded the rest of the nesting. ## Suspend (generators, async, await) **Bytecode (designed, not built).** A generator or async function owns a `GeneratorState { BcFunc, pc, JsValue[] saved, Env, This, state }`. Calling it creates the state and runs nothing (generators) or runs to the first `await` (async). `yield`/`await` instructions copy the frame's register window (NumRegs slots, typically 10 to 40) into `saved`, store pc and env, pop the frame and return from the running `Run` invocation with a "suspended" result. `next(v)` pushes a frame at the stack top, copies `saved` in, writes `v` into the register the yield names and continues at the saved pc. `throw(e)` and `return(v)` resume at the same pc with a mode flag that a `Resume` instruction after each yield checks: throw raises at that pc (so the handler table applies), return jumps to the function's cleanup path (finally blocks are compiled as handlers that rethrow or continue a stored completion). `yield*` and `for await` are loops around the same instructions. Promise reactions are host jobs that resume the state; an async function awaiting records the awaited promise in its state, which is what stitches async stacks. Cost: a window copy per suspend and resume. **Closure tree.** The options, none of them good: (1) a resumable second form of each node keeping its progress in frame-side state slots and re-walking from the root on resume; roughly doubles the node set and slows both forms; (2) a thread per generator, which breaks determinism and costs a thread per live generator; (3) lowering generator bodies to a flat basic-block form, which is a bytecode compiler plus the interpreter for it, i.e. the hybrid. ## Replay and tracing - Lean run: spans only. Bytecode keeps the pc-to-span table (two ints per instruction); the closure tree keeps start and end on each node. - Traced run (bytecode): `Run` with `TTrace : struct, ITracer`; `OnOp(frame, instruction, pc, stack, sp)` is called before every instruction. `NoTrace` is empty and inlined away. `RecordingTrace` acts on the stores only (`InitSlot`, `StoreSlot`, `StoreSlotChecked`, `InitGlobalLexical`, `StoreGlobal`, `DefineField`): the compiler keeps a side table (`FunctionCode.ValueSpans`, instruction index to the span of the value written) and the tracer records, per binding (environment and slot, or global name) and per object-literal property, the span the value was written from; an object that is stored again keeps the chain of earlier places (cap 8). The explainer (`Diagnostics/Explain.cs`) reads that state and the live environments of the failing frames (captured on `StackFrameInfo` while the error is created) to build labels, "did you mean" and locals. It reads data properties only and never runs script. - The traced run is a replay: the record log supplies clock, randomness and host results, so the same instructions run in the same order. Because both instantiations come from one source, they cannot drift apart. - Closure tree: a decorating node factory for the second tree; fused nodes need to be disabled in it (see the table). ## Host-JIT behaviour worth carrying into the real engine - The dispatch loop is one very long-running method. Without `MethodImplOptions.AggressiveOptimization` it spends its first calls in unoptimised code and on-stack replacement; with it, a single cold run of the bytecode executor dropped by 35 to 45% on the scripts tried (fib 76 to 44 ms, loops 92 to 57 ms, informal local timing). It costs about 10 ms of JIT up front on a `1 + 1` script. NativeAOT has no such phase. - The closure tree's many small methods stay in unoptimised code until the runtime's tiering delay passes (100 ms), so one-shot scripts shorter than that run unoptimised; marking the loop statements as aggressively optimised did not help. ## Measured Sources: `SpikeScripts` (fib(25); 1,000x1,000 nested loops; property get/set; closures, `map`/`filter` written in script and a counter factory; string building with `charCodeAt`; a vector class with `new` and prototype methods; a recursive-descent calculator over a string; quicksort on 40,000 numbers; 10,000 throw/catch). Each iteration creates a fresh realm or engine from an already parsed program, so realm set-up, compilation and execution are inside the number. Ratios are mean time divided by Jint's. Allocation is managed bytes per operation. ### Steady state (BenchmarkDotNet 0.15.8, out-of-process, 3 warm-up and 10 measured iterations) | script | Jint mean (ms) | Jint alloc | A closure mean (ms) | A alloc | A / Jint | B bytecode mean (ms) | B alloc | B / Jint | B / A | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:| | startup | 0.001 | 8 KB | 0.002 | 12 KB | 1.43 | 0.002 | 17 KB | 1.83 | 1.28 | | fib25 | 30.158 | 37 KB | 4.874 | 25 KB | 0.16 | 7.962 | 25 KB | 0.26 | 1.63 | | loops | 66.312 | 89.4 MB | 12.616 | 19 KB | 0.19 | 18.492 | 24 KB | 0.28 | 1.47 | | props | 55.497 | 41.7 MB | 16.269 | 24 KB | 0.29 | 22.380 | 34 KB | 0.40 | 1.38 | | closures | 37.566 | 5.3 MB | 10.509 | 4.8 MB | 0.28 | 15.695 | 4.8 MB | 0.42 | 1.49 | | strings | 2.941 | 4.5 MB | 2.565 | 3.7 MB | 0.87 | 2.810 | 3.7 MB | 0.96 | 1.10 | | vectors | 88.959 | 68.2 MB | 15.202 | 24.4 MB | 0.17 | 23.285 | 24.5 MB | 0.26 | 1.53 | | calc | 112.767 | 6.9 MB | 21.578 | 431 KB | 0.19 | 35.341 | 446 KB | 0.31 | 1.64 | | qsort | 107.468 | 27.9 MB | 22.868 | 2.0 MB | 0.21 | 23.728 | 2.0 MB | 0.22 | 1.04 | | throws | 31.840 | 14.3 MB | 17.621 | 6.0 MB | 0.55 | 1.400 | 1.3 MB | 0.04 | 0.08 | The `startup` row (`1 + 1`) is realm construction: 2 microseconds for both executors, against Jint's 1. An earlier run of the same harness (before the dispatch loop was marked aggressively optimised and the frame pools became lazy) gave ratios to Jint within 0.1 of these on every script (closure tree 0.15 to 0.97, bytecode 0.21 to 1.01, except `throws` 0.52 and 0.04 and `startup`), so the conclusions do not depend on the noise below. ### Cold, one script per fresh process (parse included, median of 5, ms and peak resident set in MB) | script | Jint ms | Jint peak RSS MB | A ms | A RSS MB | B ms | B RSS MB | |---|---:|---:|---:|---:|---:|---:| | startup | 64 | 47 | 15 | 33 | 28 | 40 | | fib25 | 255 | 50 | 59 | 36 | 44 | 43 | | loops | 441 | 98 | 173 | 36 | 57 | 43 | | props | 324 | 95 | 126 | 37 | 90 | 43 | | closures | 318 | 56 | 105 | 42 | 63 | 48 | | strings | 117 | 55 | 40 | 40 | 47 | 46 | | vectors | 415 | 98 | 118 | 61 | 78 | 67 | | calc | 449 | 60 | 190 | 38 | 119 | 43 | | qsort | 425 | 80 | 169 | 38 | 82 | 45 | | throws | 195 | 72 | 76 | 51 | 44 | 44 | A fresh process pays JIT time: the closure tree runs most of a short script in unoptimised code, the bytecode loop (marked aggressively optimised) pays about 10 ms up front and then runs optimised. Under NativeAOT there is no such phase. ### Reachable call depth (host thread stack, `MaxDepth` raised to 1,000,000) | host stack | closure tree | bytecode | |---|---:|---:| | 1 MB | 854 | 400,000 (probe cap) | | 8 MB | 7,689 | 400,000 (probe cap) | | 64 MB | 232,542 | 400,000 (probe cap) | Front-end acceptance of 100,000-link chains on a 256 KB thread: binary operators, member accesses and calls parse; a chain of 100,000 nested assignments stops with the parser's own nesting limit. ### Method and caveats - Machine: AMD Ryzen 9 7950X3D, 16 cores / 32 threads, Linux x86-64, .NET SDK 10.0.402, runtime 10.0.13, BenchmarkDotNet 0.15.8, Jint 4.17.0, Release builds with default tiering and dynamic PGO. The runs were serial and single-threaded. - Load: the machine was shared and other jobs ran during every run (load average between 4 and 14 on 32 threads), so these numbers are indicative, not publishable; repeat on an idle machine before quoting absolute times. Reproduce with `bench/OneEcma.Benchmarks` (`--filter '*ExecutorBenchmarks*' '*ValueRepBenchmarks*'`, then `cold