diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af64d758..c2288c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,15 +45,6 @@ jobs: # native MTP --coverage flag is ignored in this mode). The collector # ships with the MSTest meta-package, so no extra dependency is needed. run: dotnet test --no-build -c Release --logger "trx;LogFileName=test.trx" --collect:"Code Coverage;Format=cobertura" - - name: Test (Jint alt-engine — Engine suite under STARLING_JS_ENGINE=jint) - # The default test run above exercises the in-house Starling.Js backend. - # Jint is a runtime-selectable alternative (a temporary compat crutch); - # re-run the engine integration suite against it so the alt backend can - # never silently regress. The Jint backend's own unit tests - # (Starling.Bindings.Jint.Tests) already ran in the default step. - env: - STARLING_JS_ENGINE: jint - run: dotnet test tests/Starling.Engine.Tests/Starling.Engine.Tests.csproj --no-build -c Release --logger "trx;LogFileName=test-jint.trx" - name: Test (golden-image) run: dotnet test --no-build -c Release --filter Category=GoldenImage - name: Upload coverage to Codecov @@ -117,7 +108,6 @@ jobs: src/Starling.Js/ \ src/Starling.Js.Hosting/ \ src/Starling.Bindings/ \ - src/Starling.Bindings.Jint/ \ src/Starling.Loop/ \ src/Starling.Engine/ diff --git a/AGENTS.md b/AGENTS.md index 850276a2..736d8a7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,16 +163,14 @@ Runtime selection flags go after `aspire run --` and are forwarded to both GUI and headless resources: ```bash -aspire run -- --jint --imagesharp # Jint JS backend + CPU paint +aspire run -- --starling --imagesharp # Starling JS backend + CPU paint aspire run -- --starling --gpu # Starling JS backend + WebGPU paint ``` Flags win over `STARLING_JS_ENGINE` and `STARLING_PAINT_BACKEND`. -The default JS engine is **Starling**. Pass `--jint` (or set -`STARLING_JS_ENGINE=jint`) to use the Jint backend instead. Binding / DOM / -JS-OM work should target `src/Starling.Bindings` and its matching tests unless -the task is explicitly about the Jint backend. +The JS engine is **Starling** — the only backend. Binding / DOM / JS-OM work +targets `src/Starling.Bindings` and its matching tests. The default HTML parser is the **Starling parser**. The Aspire resources and the desktop `Starling.Gui` startup both default to it. Pass `--anglesharp-html` (or diff --git a/Directory.Build.props b/Directory.Build.props index c88de09c..cd242449 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,7 @@ net11 and the preview language version. So Starling.Bindings and every project that consumes it move to net11. Projects below Bindings (Dom, Js, Common, and so on) stay on net10 and are referenced downlevel, which the - runtime allows. The Jint backend does not reference Bindings, so it stays - on net10 too. Dom.Tests stays on net10, so the core DOM tests run + runtime allows. Dom.Tests stays on net10, so the core DOM tests run unchanged. The Aspire host (Starling.AppHost) stays on net10: its Aspire.AppHost.Sdk diff --git a/Directory.Packages.props b/Directory.Packages.props index 54dd5920..8b684209 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -83,13 +83,6 @@ - - @@ -15,7 +14,6 @@ - diff --git a/bench/Starling.JsEngineBench/StarlingFeatureBench.cs b/bench/Starling.JsEngineBench/StarlingFeatureBench.cs index afb40878..82e5801b 100644 --- a/bench/Starling.JsEngineBench/StarlingFeatureBench.cs +++ b/bench/Starling.JsEngineBench/StarlingFeatureBench.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -using Jint; using Starling.Js.Bytecode; using Starling.Js.Parse; using Starling.Js.Runtime; @@ -9,10 +8,9 @@ namespace Starling.JsEngineBench; /// /// Starling-authored microbenchmarks, one per engine optimization, run through -/// the SAME Starling-vs-Jint harness as -/// (cold + prepared, Jint as the baseline). Where the vendored dromaeo suite -/// conflates many things, each case here isolates one piece of work so its -/// standing against Jint is visible and trackable on its own: +/// the SAME harness as (cold + prepared). Where +/// the vendored dromaeo suite conflates many things, each case here isolates +/// one piece of work so its cost is visible and trackable on its own: /// /// calls — call-argument and frame-stack pooling. /// prop-read-mono — monomorphic own-property read inline cache. @@ -35,7 +33,7 @@ namespace Starling.JsEngineBench; [RankColumn] public class StarlingFeatureBench { - // Strict-mode parity with EngineComparisonBench's prelude (no dromaeo stubs + // Strict-mode parity with ScriptSuiteBench's prelude (no dromaeo stubs // needed — these scripts are self-contained). private const string Prelude = "\"use strict\";\n"; @@ -109,8 +107,8 @@ public class StarlingFeatureBench "for (var i = 0; i < 100000; i++) { s += str.split(/,/).length; }\n" + "s;", - // Regex global replace: the @@replace path (still slower than Jint — - // tracks the next regex lever). + // Regex global replace: the @@replace path (the known remaining regex + // gap — tracks the next regex lever). ["regex-replace"] = "var str = 'the quick brown fox jumps over';\n" + "var s = 0;\n" + @@ -129,39 +127,26 @@ public class StarlingFeatureBench public static IEnumerable Cases => Scripts.Keys; private string _src = ""; - private Chunk? _starlingChunk; - private Prepared _jintPrepared; + private Chunk? _chunk; [GlobalSetup] public void Setup() { _src = Prelude + Scripts[Case]; - _starlingChunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram()); - _jintPrepared = Engine.PrepareScript(_src, strict: true); + _chunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram()); - // Fail fast if a case throws on either engine, so a broken script never - // masquerades as a (mis)measured benchmark. - RunStarling(); - RunJint(); + // Fail fast if a case throws, so a broken script never masquerades as a + // (mis)measured benchmark. + RunCold(); } [Benchmark(Baseline = true)] - public Engine Jint() => RunJint(); - - [Benchmark] - public Engine Jint_ParsedScript() => - new Engine(o => o.Strict = true).Execute(_jintPrepared); - - [Benchmark] - public JsValue Starling() => RunStarling(); + public JsValue Starling() => RunCold(); [Benchmark] public JsValue Starling_Prepared() => - new JsVm(new JsRuntime()).Run(_starlingChunk!); + new JsVm(new JsRuntime()).Run(_chunk!); - private JsValue RunStarling() => + private JsValue RunCold() => new JsVm(new JsRuntime()).Run(JsCompiler.CompileForEval(new JsParser(_src).ParseProgram())); - - private Engine RunJint() => - new Engine(o => o.Strict = true).Execute(_src); } diff --git a/bench/Starling.JsEngineBench/StarlingScalingBench.cs b/bench/Starling.JsEngineBench/StarlingScalingBench.cs index dc23359e..8dcbba59 100644 --- a/bench/Starling.JsEngineBench/StarlingScalingBench.cs +++ b/bench/Starling.JsEngineBench/StarlingScalingBench.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Attributes; -using Jint; using Starling.Js.Bytecode; using Starling.Js.Parse; using Starling.Js.Runtime; @@ -8,22 +7,20 @@ namespace Starling.JsEngineBench; /// /// Scaling sweep: ONE representative workload run at an increasing iteration -/// count N, so the engines' fixed cost (parse + compile + realm +/// count N, so the engine's fixed cost (parse + compile + realm /// bootstrap) and marginal per-iteration dispatch cost separate out as /// N grows. Where pins each feature at a /// single large N, this one answers "one-off vs medium vs lots-and-lots". /// -/// Read the table down an engine's column as N increases: +/// Read the table down a column as N increases: /// -/// N = 1 — dominated by FIXED cost (one-off latency). A bytecode VM -/// pays a compile step a tree-walker skips, so Jint is expected to win here. -/// N large — dominated by MARGINAL cost (steady-state throughput). -/// This is where bytecode + inline caches are expected to pull ahead — IF -/// per-instruction dispatch is cheaper than re-walking the AST. A crossover -/// exists only if Starling's marginal cost is below Jint's. +/// N = 1 — dominated by FIXED cost (one-off latency): the parse + +/// compile + bootstrap a bytecode VM pays before the first instruction. +/// N large — dominated by MARGINAL cost (steady-state throughput), +/// where bytecode + inline caches earn their keep. /// /// -/// Decompose per engine from the curve: +/// Decompose from the curve: /// fixed ≈ time at N=1; /// marginal ≈ (t(N_hi) − t(N_lo)) / (N_hi − N_lo). /// @@ -35,9 +32,9 @@ namespace Starling.JsEngineBench; /// The body is a monomorphic property + prototype-method + call workload — the /// shape of code a bytecode VM with inline caches is supposed to be strongest /// on, and (unlike the regex cases) no shared System.Text call dilutes the -/// engines' own dispatch. Both Warm and Cold build a fresh engine/realm per op, +/// engine's own dispatch. Both Warm and Cold build a fresh runtime/realm per op, /// so realm bootstrap stays part of the fixed cost (intentional — it is part of -/// one-off latency). Uses a short job (4 methods × 4 N); run with +/// one-off latency). Uses a short job (2 methods × 4 N); run with /// --filter '*StarlingScalingBench*'. /// [ShortRunJob] @@ -63,28 +60,19 @@ private static string Build(int n) => private string _src = ""; private Chunk? _chunk; - private Prepared _jintPrepared; [GlobalSetup] public void Setup() { _src = Build(N); _chunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram()); - _jintPrepared = Engine.PrepareScript(_src, strict: true); - // Fail fast if a script throws on either engine. - Jint_Warm(); + // Fail fast if the script throws. Starling_Warm(); } - // Baseline: warm Jint (pre-parsed AST), the natural steady-state reference. + // Baseline: warm (pre-compiled chunk), the natural steady-state reference. [Benchmark(Baseline = true)] - public Engine Jint_Warm() => new Engine(o => o.Strict = true).Execute(_jintPrepared); - - [Benchmark] - public Engine Jint_Cold() => new Engine(o => o.Strict = true).Execute(_src); - - [Benchmark] public JsValue Starling_Warm() => new JsVm(new JsRuntime()).Run(_chunk!); [Benchmark] diff --git a/bench/engine-comparison.md b/bench/engine-comparison.md index 8ca2db91..7fd045c3 100644 --- a/bench/engine-comparison.md +++ b/bench/engine-comparison.md @@ -1,12 +1,11 @@ -# JS engine comparison: Starling vs Jint +# JS engine script-suite results -Where does the Starling JS engine land against Jint? This runs Jint's own -"EngineComparison" benchmark on our engine. Same 19 scripts, same strict mode, -ranked on one machine so the numbers compare fairly. +How fast is the Starling JS engine on real script workloads? This runs the 19 +vendored scripts under `bench/Starling.JsEngineBench/Scripts/` (dromaeo loops, +base64, a 34 KB linq library, regular expressions, `eval`-heavy code) in strict +mode, ranked on one machine. -The benchmark lives in `bench/Starling.JsEngineBench`. The scripts under -`Scripts/` are copied straight from Jint's suite (see `Scripts/README.md` for the -source commit). +The benchmark lives in `bench/Starling.JsEngineBench` (`ScriptSuiteBench`). ## How to run @@ -18,138 +17,63 @@ dotnet run -c Release --project bench/Starling.JsEngineBench -- --filter '*' dotnet run -c Release --project bench/Starling.JsEngineBench -- --job dry --filter '*' ``` -Each engine runs two ways, matching Jint's own split: +Each script runs two ways: -- **cold** — parse, compile, and run every time. Read `Starling` against `Jint`. -- **prepared** — compile once, then run on a fresh runtime each time. Read - `Starling_Prepared` against `Jint_ParsedScript`. +- **cold** (`Starling`) — parse, compile, and run every time. +- **prepared** (`Starling_Prepared`) — compile once, then run on a fresh + runtime each time. + +The full table with error bars and per-generation garbage-collector counts is +written to `BenchmarkDotNet.Artifacts/results/` under whatever folder you run +the command from. That folder is gitignored. ## What we found Starling ran **all 19 scripts with zero failures** — modern syntax, `eval`, -`new Function`, regular expressions, and the 34 KB `linq-js` library. For a young -engine that is the headline. Jint passes everything too, so this is a clean -head-to-head. - -On speed, Starling is the slower engine on every script. The gap is small on -some, huge on others: - -- **Close (1.2x–2x):** the dromaeo 3d-cube and object-string scripts. Tight - arithmetic and string building. Starling's bytecode interpreter keeps up well - here. -- **Middle (3x–7x):** core-eval, base64, stopwatch, linq, plain evaluation. The - common case. -- **Two blow-ups:** the regular-expression scripts run about **120x slower** — - almost 13 seconds against Jint's 0.1 second. This is the Starling - regular-expression engine, not the interpreter. It is the single biggest thing - to fix. - -One bright spot: on `linq-js`, prepared Starling (182 µs) beats **cold** Jint -(1,000 µs). Reuse the compiled script and Starling does fine on real library code. - -The bigger weakness is memory. Starling allocates far more than Jint on most -scripts. The range is wide: nearly even on the object-string scripts (1.1x), up -to 346x on the regular-expression scripts. A no-JIT bytecode interpreter does -more boxing and short-lived allocation per operation, and it shows. This is the -other thing to chase, because high allocation drives garbage-collector pauses -that hurt frame time in the browser. - -(JIT = just-in-time compiling to native code. Jint stays an interpreter too, so -the gap here is about allocation, not native codegen.) - -## Local results (this machine) - -Apple M3 Max, macOS 26.3, .NET 10.0.8 Arm64. BenchmarkDotNet 0.14.0, ShortRun (3 -launches, 3 warmup, 3 iterations). Times are mean per run. - -- **cold x** is `Starling` time divided by `Jint` time. -- **prep x** is `Starling_Prepared` time divided by `Jint_ParsedScript` time. -- **Alloc** is `Starling` bytes divided by `Jint` bytes. - -Lower is better for all three. Ranked from smallest cold gap to largest. - -| Script | Jint | Starling | cold x | prep x | Alloc | -|---|--:|--:|--:|--:|--:| -| dromaeo-3d-cube.js | 17.3 ms | 20.8 ms | 1.2x | 1.7x | 11x | -| dromaeo-object-string.js | 104 ms | 169 ms | 1.6x | 1.6x | 1.1x | -| dromaeo-object-string-modern.js | 106 ms | 180 ms | 1.7x | 1.7x | 1.2x | -| dromaeo-3d-cube-modern.js | 12.1 ms | 24.2 ms | 2.0x | 1.8x | 11x | -| stopwatch-modern.js | 172 ms | 577 ms | 3.4x | 3.3x | 286x | -| linq-js.js | 1.00 ms | 3.66 ms | 3.7x | 3.3x | 4.0x | -| dromaeo-core-eval-modern.js | 1.93 ms | 7.44 ms | 3.9x | 3.9x | 95x | -| dromaeo-core-eval.js | 1.79 ms | 7.23 ms | 4.0x | 4.1x | 95x | -| dromaeo-string-base64.js | 20.5 ms | 91.0 ms | 4.4x | 4.5x | 335x | -| dromaeo-string-base64-modern.js | 23.4 ms | 102 ms | 4.4x | 4.0x | 338x | -| stopwatch.js | 155 ms | 746 ms | 4.8x | 4.9x | 284x | -| evaluation-modern.js | 17 µs | 111 µs | 6.5x | 16x | 14x | -| evaluation.js | 16 µs | 113 µs | 6.9x | 18x | 14x | -| array-stress.js | 3.08 ms | 22.1 ms | 7.2x | 6.4x | 65x | -| dromaeo-object-array-modern.js | 15.0 ms | 164 ms | 10.9x | 11.0x | 128x | -| dromaeo-object-array.js | 13.8 ms | 162 ms | 11.8x | 12.0x | 126x | -| minimal.js | 7.5 µs | 97 µs | 13.0x | 31x | 18x | -| dromaeo-object-regexp-modern.js | 110 ms | **12.5 s** | **114x** | 130x | 346x | -| dromaeo-object-regexp.js | 104 ms | **12.9 s** | **124x** | 163x | 344x | - -Two notes on prepared mode: - -- On the tiny scripts (`minimal`, `evaluation`) prepared mode looks *worse*, not - better. Jint's parsed-script path drops to a few microseconds, while Starling - still pays a fixed per-run setup cost. The ratio grows because Jint's - denominator shrank, not because Starling slowed down. -- `linq-js` is the win. Prepared Starling (182 µs) beats cold Jint (1,000 µs). - Against Jint's own prepared path (55 µs) it is still 3.3x behind, but the - compiled-artifact reuse clearly pays off on real library code. - -The full table with error bars and per-generation garbage-collector counts is -written to `BenchmarkDotNet.Artifacts/results/` under whatever folder you run the -command from. That folder is gitignored. - -## Published reference numbers (the four-engine field) - -We only run Starling and Jint locally, so those two columns above are the -trustworthy same-machine pair. The other three engines in Jint's suite — -NiL.JS, Jurassic, and YantraJS — are not run here. The numbers below are Jint's -own published table, copied verbatim for reference. - -**Different hardware. Treat as a rough guide, not a same-machine ranking.** -Jint's board ran on an AMD Ryzen 9 5950X under Windows 11 with .NET 10.0.7 -(BenchmarkDotNet 0.15.8, last updated 2026-05-10). Our local board is an Apple -M3 Max under macOS. Even the Jint column differs from ours for that reason — for -example regexp reads 135 ms there against 104 ms here. - -Cold `Jint` mean per run, base (non-`modern`) scripts: - -| Script | Jint | NiL.JS | Jurassic | YantraJS | -|---|--:|--:|--:|--:| -| minimal | 2.7 µs | 2.8 µs | 2,305 µs | 153 µs | -| evaluation | 15 µs | 26 µs | 2,110 µs | 156 µs | -| linq-js | 1.20 ms | 3.97 ms | 36.2 ms | 0.34 ms | -| dromaeo-core-eval | 2.46 ms | 1.23 ms | 17.1 ms | 4.54 ms | -| array-stress | 3.60 ms | 4.85 ms | 9.15 ms | 15.3 ms | -| dromaeo-3d-cube | 12.4 ms | 6.19 ms | 55.1 ms | 3.00 ms | -| dromaeo-object-array | 18.5 ms | 52.2 ms | 35.4 ms | 65.7 ms | -| dromaeo-string-base64 | 26.9 ms | 25.9 ms | 47.1 ms | 43.0 ms | -| dromaeo-object-string | 155 ms | 128 ms | 205 ms | 173 ms | -| stopwatch | 195 ms | 132 ms | 142 ms | 63.4 ms | -| dromaeo-object-regexp | 135 ms | 528 ms | 678 ms | 1,060 ms | - -The pattern from that board: Jint and NiL.JS trade the top spot on most scripts. -YantraJS wins the graphics-heavy 3d-cube and the stopwatch loops, but allocates -enormous amounts of memory (over 1 GB on object-array, against Jint's 10 MB). -Jurassic is slow to start and weak on `eval`-style scripts. - -One thing stands out for our roadmap: even the *slowest* published engine on -regexp, YantraJS at about 1.06 seconds, is still roughly 12x faster than -Starling's 12.9 seconds. The regular-expression gap is not a Starling-vs-Jint -problem. It is last place against the whole field. - -## Where this puts Starling - -Slotting Starling in by its ratio to Jint: on the close and middle scripts (1.2x -to 7x), Starling lands near Jurassic's tier — behind Jint and NiL.JS. On the -regular-expression scripts it would sit dead last by a wide margin until the -Starling regular-expression engine is fixed. On `linq-js` with a prepared -script, Starling is genuinely competitive. - -So: a solid mid-pack interpreter that already runs everything, with two clear -work items — the regular-expression engine, and allocation per operation. +`new Function`, regular expressions, and the 34 KB `linq-js` library. For a +young engine that is the headline. + +Timings on Apple M3 Max, macOS 26.3, .NET 10.0.8 Arm64 (BenchmarkDotNet, +ShortRun; mean per cold run): + +| Script | Starling | +|---|--:| +| minimal.js | 97 µs | +| evaluation.js | 113 µs | +| evaluation-modern.js | 111 µs | +| linq-js.js | 3.66 ms | +| dromaeo-core-eval.js | 7.23 ms | +| dromaeo-core-eval-modern.js | 7.44 ms | +| dromaeo-3d-cube.js | 20.8 ms | +| array-stress.js | 22.1 ms | +| dromaeo-3d-cube-modern.js | 24.2 ms | +| dromaeo-string-base64.js | 91.0 ms | +| dromaeo-string-base64-modern.js | 102 ms | +| dromaeo-object-array.js | 162 ms | +| dromaeo-object-array-modern.js | 164 ms | +| dromaeo-object-string.js | 169 ms | +| dromaeo-object-string-modern.js | 180 ms | +| stopwatch-modern.js | 577 ms | +| stopwatch.js | 746 ms | +| dromaeo-object-regexp.js | **12.9 s** | +| dromaeo-object-regexp-modern.js | **12.5 s** | + +Two clear work items fall out of the ranking: + +1. **The regular-expression engine.** The two regexp scripts take about 13 + seconds while everything else stays under a second. This is the + regular-expression engine, not the interpreter, and it is the single biggest + thing to fix. +2. **Allocation per operation.** A no-JIT bytecode interpreter does more boxing + and short-lived allocation per operation, and the memory columns show it — + worst on the regexp and stopwatch scripts. High allocation drives + garbage-collector pauses that hurt frame time in the browser. + +One bright spot: on `linq-js`, the prepared path (182 µs) shows the +compiled-artifact reuse paying off on real library code — a ~20x drop from the +cold run. + +A note on prepared mode: on the tiny scripts (`minimal`, `evaluation`) prepared +mode saves little, because a fixed per-run setup cost (fresh runtime + realm +bootstrap) dominates. The `bootstrap` case in `StarlingFeatureBench` tracks that +cost in isolation. diff --git a/bench/html-parser-comparison.md b/bench/html-parser-comparison.md index 79f6331c..07ebde5a 100644 --- a/bench/html-parser-comparison.md +++ b/bench/html-parser-comparison.md @@ -3,8 +3,8 @@ Where does the Starling HTML parser land against a mature, pure-managed reference parser? This runs Starling.Html and [AngleSharp](https://github.com/AngleSharp/AngleSharp) on the same pages, on one machine, ranked together so the numbers compare -fairly. It is the HTML-parsing counterpart of `engine-comparison.md`, which does -the same for the Starling JS engine against Jint. +fairly. It is the HTML-parsing counterpart of `engine-comparison.md`, which +measures the Starling JS engine on its script suite. The benchmark lives in `bench/Starling.HtmlParserBench`. AngleSharp is a dev-only dependency there. No engine project references it, so the managed-first diff --git a/browser-plan/00_INDEX.md b/browser-plan/00_INDEX.md index 0eb0cb51..d7d58477 100644 --- a/browser-plan/00_INDEX.md +++ b/browser-plan/00_INDEX.md @@ -26,7 +26,7 @@ | Native code | None. Pure managed. `System.Security.Cryptography` BCL primitives are allowed; everything above the primitive layer is hand-written. | user | | UI | Avalonia 12 (stable 12.0.x, released Apr 2026; targets .NET 10 directly; .NET 8+ only) | user | | Rasterization | `SixLabors.ImageSharp` 3.x + `SixLabors.ImageSharp.Drawing` 2.x + `SixLabors.Fonts` 2.x | user | -| JS engine | Hand-written in C#. Jint and Acornima may be consulted as references but are **not dependencies**. | user | +| JS engine | The Starling JS engine, written from scratch in C#. No third-party JS engine dependencies. | user | | Networking | Hand-written from `System.Net.Sockets` up. No `HttpClient`, no `SslStream`. | user | | Process model | Single-process for v1. Ladybird-style multi-process sandboxing deferred to v2. | this plan | | Cross-platform | Windows + macOS + Linux from day one. No platform branches without an `OPEN QUESTION`. | user | diff --git a/browser-plan/09_JS_ENGINE.md b/browser-plan/09_JS_ENGINE.md index 50a3c4ed..b30ff5c7 100644 --- a/browser-plan/09_JS_ENGINE.md +++ b/browser-plan/09_JS_ENGINE.md @@ -7,76 +7,37 @@ ## Goal posture -**Hand-write everything.** Reference implementations to read (not copy): +**Write everything in-house.** Reference implementations to read (not copy): - `Acornima` (C# port of Acorn) — clean ESTree-style parser. -- `Jint` (C#) — interpreter, env records, intrinsics. - `Boa` (Rust) — modern register VM design. - `LibJS` (Ladybird) — spec-faithful bytecode VM. These are read for **structure** and **algorithm**, not copied. Our shape is closer to LibJS: AST → bytecode → register VM. -## Alternative engine backend (Jint) - -The browser can run on a second, runtime-selectable JS engine: **[Jint](https://github.com/sebastienros/jint)**, -a pure-managed C# ECMAScript interpreter. It is a **temporary compatibility -crutch** — it lets Starling render real-world pages at near-full ECMAScript -conformance *today* while the in-house `Starling.Js` engine climbs toward its -own conformance target. It is meant to be removed once `Starling.Js` is good -enough; the architecture is built so removal is a one-step deletion. - -- **Why Jint fits the interop policy.** Jint is pure-managed (its only dependency - is the managed Acornima parser; no `runtimes/` native assets, no P/Invoke), so - it satisfies the managed-first rule exactly like BouncyCastle. Both new - projects are in the CI interop-seam allowlist. -- **Selection.** Set `STARLING_JS_ENGINE=jint` (default `starling`). The selector - (`Starling.Engine/JsEngineSelector.cs`) mirrors `PaintBackendSelector`: lazy, - default-on-unset, loud-fail on an unknown value. -- **Conformance delta (measured on tc39/test262 `language`, identical corpus).** - Jint ≈ **99.6%** vs `Starling.Js` ≈ **81%** — about a 19-point web-compat gap - that the crutch closes while the in-house engine catches up. Run both numbers - yourself via the `Conformance_pass_rate` / `Jint_conformance_pass_rate` tests - in `tests/Starling.Js.Test262.Tests` (corpus fetched by `tools/fetch-test262.sh`). - -### Architecture — the narrow seam - -The engine-neutral shared asset is `Starling.Dom` (the real DOM); both engines -wrap *the same* Dom nodes, only the marshalling differs. So the abstraction lives -at the `Starling.Engine` ↔ JS boundary, **not** at the `JsValue`/`JsObject` -level — the ~956 existing `Starling.Bindings` call sites are untouched. +## Engine backend seam + +The Starling JS engine is the only JS backend. The engine still talks to it +through a narrow, engine-neutral seam so the browser's orchestration never +depends on engine internals. + +The engine-neutral shared asset is `Starling.Dom` (the real DOM); the backend +wraps the same Dom nodes. The abstraction lives at the `Starling.Engine` ↔ JS +boundary, **not** at the `JsValue`/`JsObject` level. - `src/Starling.Js.Hosting` — the seam: `IScriptEngineFactory`, `IScriptSession`, `ScriptSessionOptions`, `ScriptThrow`, and the shared `ILayoutHost`. Depends - only on Dom/Net/Common/Url; references neither engine. -- `src/Starling.Bindings` — hosts the default **Starling.Js** backend + only on Dom/Net/Common/Url; references no JS engine. +- `src/Starling.Bindings` — hosts the **Starling.Js** backend (`StarlingScriptSession`) over the existing `JsRuntime` path. -- `src/Starling.Bindings.Jint` — the **Jint** backend: `JintScriptSession` plus a - full, idiomatic re-exposure of the Web-API surface over Jint interop - (Node/Element/Document, EventTarget/Event, Window/Storage/History/Performance, - timers/rAF + event-loop pump, fetch, XMLHttpRequest, observers/crypto/cookies, - ES modules). References Jint + the seam + Dom/Net/Css/Html/Common/Url — **not** - `Starling.Js` or `Starling.Bindings`. `Starling.Engine` keeps all orchestration (script ordering, DOMContentLoaded/load -timing, the async pump) and talks only to `IScriptSession`. Both backends pass -the same engine integration suite (`tests/Starling.Engine.Tests`, 151/151) and -CI runs that suite under both engines. - -The full design contract and the work-package history are in -[`tasks/jint/DESIGN.md`](../tasks/jint/DESIGN.md) and -[`tasks/jint/TRACKER.md`](../tasks/jint/TRACKER.md). - -### Removal checklist (when `Starling.Js` is good enough) - -1. Delete `src/Starling.Bindings.Jint/` and `tests/Starling.Bindings.Jint.Tests/`. -2. Remove the `jint` arm from `Starling.Engine/JsEngineSelector.cs` (and its - `Starling.Bindings.Jint` project reference); the env var then only accepts - `starling`. -3. Remove the `Jint` `PackageVersion` from `Directory.Packages.props`. -4. Remove the Jint test262 harness (`Jint*Test262*` in - `tests/Starling.Js.Test262.Tests`) and the Jint files from `Starling.slnx`, - the CI interop allowlist, and the `STARLING_JS_ENGINE=jint` CI step. -5. **Keep** `src/Starling.Js.Hosting` (the seam) and `ILayoutHost` there — the - seam is a clean abstraction worth retaining even with a single engine. +timing, the async pump) and talks only to `IScriptSession`. Selection is +`STARLING_JS_ENGINE` (default and only accepted value: `starling`); the selector +(`Starling.Engine/JsEngineSelector.cs`) mirrors `PaintBackendSelector`: lazy, +default-on-unset, loud-fail on an unknown value. + +The seam and `ILayoutHost` stay even with a single engine — it is a clean +abstraction worth retaining. ## Spec refs @@ -625,4 +586,4 @@ Use [Test262](https://github.com/tc39/test262). Subset selection: language featu - [ ] `for-of` over a generator yields the expected sequence. - [ ] `Proxy` traps for `get`/`set`/`has`/`deleteProperty`/`ownKeys` fire correctly. - [ ] Stack overflow surfaces as a `RangeError` with a meaningful trace, not as a C# `StackOverflowException`. -- [ ] No `DllImport`, no `Jint` import, no `Microsoft.JScript`. `grep -rn 'DllImport\|Jint\|JScript' src/Starling.Js/` is empty. +- [ ] No `DllImport`, no third-party JS engine import. `grep -rn 'DllImport\|JScript' src/Starling.Js/` is empty. diff --git a/browser-plan/anglesharp-backend-plan.md b/browser-plan/anglesharp-backend-plan.md index fa02a0ef..f7c93cba 100644 --- a/browser-plan/anglesharp-backend-plan.md +++ b/browser-plan/anglesharp-backend-plan.md @@ -1,8 +1,8 @@ # Plan: AngleSharp as a swappable HTML-parser backend This is the agreed plan for adding AngleSharp as an opt-in, off-by-default HTML -parser, picked at runtime. It mirrors how the JS engine already switches between -the Starling engine and Jint. The Starling parser stays the default. +parser, picked at runtime. It mirrors the JS-engine backend seam. The Starling +parser stays the default. Read this top to bottom. It is self-contained, so a fresh session can pick it up and build it without the chat history. @@ -57,7 +57,6 @@ except the new backend project plus the selector wiring. It is deletable. - `src/Starling.Bindings/NodeBindings.cs` — `innerHTML` / `outerHTML` / `insertAdjacentHTML` (helper around line 2325-2328, callers near 581, 601, 1115+). -- `src/Starling.Bindings.Jint/NodeBindings.cs` — the Jint mirror (around 1209). - `src/Starling.Shell.Native/*` — demo and window render call sites (`NativePresentDemo.cs:63`, `NativeBrowserWindow.cs:186` and neighbors). @@ -65,14 +64,12 @@ except the new backend project plus the selector wiring. It is deletable. - Seam interface lives in `src/Starling.Js.Hosting` (`IScriptEngineFactory`, `IScriptSession`). It depends only on `Starling.Dom` and `Starling.Common`. -- Backends: `src/Starling.Bindings` (Starling) and - `src/Starling.Bindings.Jint` (Jint, references only the seam plus the Jint - package). Each provides a factory. +- Backend: `src/Starling.Bindings` (Starling) provides the factory. - Selector: `src/Starling.Engine/JsEngineSelector.cs` reads `STARLING_JS_ENGINE` - once, caches the choice, and builds the factory. It lives in `Starling.Engine` - because that is the only project that references both backends. -- Flags: `src/Starling.AppHost/AppHost.cs` maps `--jint` / `--starling` with a - reusable `SelectFlag` helper, strips them before Aspire, and forwards the + once, caches the choice, and builds the factory. It lives in `Starling.Engine`, + where the backend assembly is referenced. +- Flags: `src/Starling.AppHost/AppHost.cs` maps `--starling` with a + reusable `SelectFlag` helper, strips it before Aspire, and forwards the choice as an environment variable. `src/Starling.Gui/Program.cs` defaults the env var when it is unset. Flag beats env var beats default. - `Starling.Dom` already grants `InternalsVisibleTo` to `Starling.Html` and @@ -129,8 +126,7 @@ startup. - Add an `HtmlTemplateElement` (or equivalent) whose `Content` is a `DocumentFragment`, following the DOM standard for `template.content`. -- Wire `template.content` in both `src/Starling.Bindings/NodeBindings.cs` and - `src/Starling.Bindings.Jint/NodeBindings.cs`. +- Wire `template.content` in `src/Starling.Bindings/NodeBindings.cs`. - Update the Starling parser's `