neper · docs · hand-maintained

e.ui.browser — migration tracker

Vaper (pure-Dart browser, frozen, never released) is being re-implemented in Neper as the e.ui.browser control, with every reusable piece (HTML/DOM, CSS style and layout, JS engine, network policy, decoders) placed in the general-purpose library and only browsing-context code in e.web.* (see “Library or browser?”). This page tracks (1) migration of what the Dart version already does, (2) gaps the Dart version never completed, which are built directly in Neper, and (3) core Neper language/stdlib work the browser needs. Dart will not be continued, so every gap is a Neper task.

Plan written 2026-10-16 · conformance re-measured 2026-11-07: html5lib 1791/1791 (100%), WPT DOM 39,023/39,590 (98.57%), test262 40,458/40,938 (98.83%), WPT CSS ~9,900/13,377. Dart baseline for reference: 83/128 requirements (65%), WPT CSS reftests 116/122 (95%), html5lib 1549/1791 (86%), overall 78%. Dart size: ~48.5k lib LOC, ~44k test LOC, ~270 test files, 1.4k C (QuickJS shim).

Where "browser control" actually stands: the control surface is complete - a host can navigate, evaluate, query, write, wait and read the rendered result (item 7.17). What it cannot yet do is show a real site reliably, and that is a layout-and-paint problem, not a control one: the CSS reftests are ~74%, the largest gaps being grid-lanes (masonry), nested multicol and line-clamp. Control is no longer the bottleneck.

Progress by phase

PhaseScopeProgressItems

Weights for the percentages: done = 1partial = 0.5 (landed in Neper, not yet verified against the Dart oracle or incomplete)wip = 0.25todo/blocked = 0deferred items are excluded from the totals

Target architecture

Layers (bottom up)

e.ui.browser (lib/e/ui/browser.e) — one browsing context as a control: widget.Custom for paint, Region for pointer/wheel, Scope for keys/focus, Edit for text/IME/clipboard. Exposes navigate / history / find / zoom / events. Contains no engine logic.
e.web.page — browsing contexts, event loop (HTML processing model), navigation, session history + bfcache, iframes, workers.
e.js — JavaScript engine written in Neper: parser, bytecode, VM, own GC heap, builtins, ES-semantics RegExp, modules. e.web.bind — host-object bindings (DOM, CSSOM, fetch, timers, storage).
e.fmt.dom · e.fmt.css.style · e.fmt.css.layout · e.fmt.css.paint — arena node tables, style resolver, CSS box layout, display list lowered to e.gfx.scene.Builder.
Existing Neper libs reused — e.fmt.html, e.fmt.css.*, e.gfx.cssvalue, e.text.shape/layout/bidi/segment, image/font decoders, e.gfx.svg/filter/scene, e.net.* (http, tls, cookie, ws, idna, policy, ssrf), e.crypto.*, e.db, e.os, e.thread/channel.
x.web.* (drivers) — renderer process + OS sandbox, profile storage, credential store, platform keychains.

Threads and data flow

  • Engine thread owns DOM, JS heap, style, layout. Per-document arena + per-frame arena; nodes are indices into tables (no pointers across mutation).
  • UI thread runs e.ui.app; Custom.paint lowers the latest immutable display list to scene.Builder. Hand-off through e.channel, double-buffered arenas, tiled to the scene limits.
  • Net runs on e.task pool; responses arrive as events on the engine thread.
  • Later (P8) the engine thread moves into a sandboxed renderer process with the same display-list protocol over pipes/shared memory.
  • Dart's isolate boundary and RenderTreeSnapshot protocol (2.1k lines) are not ported; the in-process hand-off replaces it.

Key decisions (to be recorded as D-entries in docs/decisions.md)

#DecisionRationale / consequence
K1Three tiers, general-purpose first: reusable pieces go into the ordinary library (e.js, e.wasm, e.fmt.dom, e.fmt.css.style/layout/paint, e.net.*, e.text.*, e.ui.html_view); only what needs a browsing context stays in e.web.* (page, bindings, web APIs); only widget and input code is in e.ui.browser; platform/process code in x.web.*.Refines D5114 after the owner asked that reusable parts (WOFF, CSS, HTML, ...) benefit all library users. Test for “general”: it makes sense without a document that has an origin, scripts and a session history, and takes only data plus callbacks (measure text, load a resource). See the split table below. Mail HTML (C158), Document view (C172), help and Markdown can then share the layout engine without the browser.
K2Write the JS engine in Neper; do not embed QuickJS.Neper cannot link C objects or static archives with its own linker, cannot export symbols for callbacks, and the product claim is 100% self-hosted. D2148 already states this. QuickJS-ng in vaper/native stays a dev-time differential oracle only. This is the largest single cost in the plan (see P5).
K3All text goes through e.text.shape / e.text.layout; remove every dependency on dart:ui Paragraph.Removes Dart's biggest portability risk and allows vertical writing modes, which were blocked in Dart. Tests use the Ahem font as an asset for deterministic metrics.
K4Memory: arena per document and per frame; index-based DOM/box tables; the JS engine owns its heap and a mark-sweep GC over a byte arena.Neper has no GC and no global allocator. Index tables survive mutation and cross the thread hand-off cheaply.
K5Paint = Dart-style display list, lowered to e.gfx.scene.Builder in viewport tiles.The scene has hard limits (bounded SCENE_BYTES, MAX_EDGES per command, 8 clip masks, 4 opacity layers, TooLarge result). A full page cannot be one scene. Missing scene features (blend, backdrop read-back, glyph cache) are core tasks (track "core").
K6Freeze Vaper as an oracle, then stop touching it.Before retirement capture an offline site corpus, Dart render PNGs, node dumps and display-list dumps, and export test vectors as language-neutral fixtures (P0). Neper results are compared against these, not against a live Dart build.
K7Do not port: package:html wrapper, siphash/hkdf/chacha (Neper's e.crypto has vectors), Brotli and IDNA (exist), isolates, FFI, Flutter shell/render widgets.Each has a Neper equivalent or is Dart-specific.
K8Scope of the control: one browsing context. Tabs, omnibox, bookmarks, history, downloads and DevTools are an app layer on top (NeperOS Browser, C159, then a desktop shell).e.ui.app allows one window per app today; multi-window needs a core change or one process per window.
K9This page is hand-maintained and sits beside docs/progress.html.AGENTS.md makes progress.html the only readiness document and generated. This tracker is an explicit exception for one large programme; work is still sequenced through docs/work-queue.json items (a queue entry per milestone, referencing the IDs below). docs/vaper.md is superseded for layout, paint and JS.

Library or browser? The split

Rule of thumb, in order: (1) usable without a URL, origin, script realm or session history → general-purpose library; (2) needs only data and callbacks, no widget → library; (3) needs browsing-context state → e.web.*; (4) needs pixels, input or widgets → e.ui.*; (5) needs the OS (process, sandbox, keychain) → x.web.*. Public APIs of the library modules never mention “browser”, “tab” or “page”.

PieceHomeOther users and notes
General-purpose library (outside the browser)
HTML tokenizer, tree builder, serializer, sanitizer, accessibilitye.fmt.html (exists)Mail, docs, templates, scraping. Conformance work (adoption agency, foster parenting, template) lands here for everyone.
Mutable DOM tree: nodes, attributes, classList, mutation, querySelector, innerHTML, cloneNode, serialization, tree-walkerse.fmt.dom (new)Scraping, tests, XSLT/XPath, mail, server-side rendering. No events, no scripts, no window.
CSS syntax, selectors, cascade kernels, container/scope, valuese.fmt.css.*, e.gfx.cssvalue (exist)Already general; keep adding vectors here, not in the browser.
Style resolver: inheritance, shorthands, var(), @media/@supports/@layer, UA sheet, computed valuese.fmt.css.style (new)Mail, PDF, reports, html_view. Dynamic pseudo-classes (:hover, :focus) come in through a state callback, so the library has no input model.
CSS box layout: block, inline, float, positioned, table, flex, grid, multicol; fragments and hit testinge.fmt.css.layout (new)Input is a styled tree plus a text-measure callback and image sizes. Gives HTML-to-PDF with e.fmt.pdf, print, mail, help, reports.
Display list and its lowering to e.gfx.scene in tilese.fmt.css.paint (new)Same consumers. Scene limits are handled here, once.
Static HTML/CSS view control (no JS, no network)e.ui.html_view (new)Same shape as e.ui.markdown_view. NeperOS Mail (C158) and Document (C172) use it; e.ui.browser builds on it. Also the M1 stepping stone.
JavaScript engine: parser, compiler, VM, heap, builtins, modules; ECMAScript RegExp; Intl subsete.js, e.js.regexp (new)Scripting, config, plugins, spreadsheet formulas, tests. Knows nothing of the DOM; hosts supply objects through a host-object API.
WebAssembly decoder, validator, interpretere.wasm (new)Plugin sandboxes; the browser only adds the JS API.
Network: request pipeline (redirects, referrer policy, sniffing), HTTP cache RFC 9111, HTTP/2 transport, TLS 1.2, HSTS, public suffix list, CSP, CORS, COOP/COEP/CORP, filter lists, DoH, proxye.net.* (some exist)Every HTTP client and server gains caching and policy. Policy engines are pure (header + context in, verdict out); enforcement wiring to a document stays in e.web.
Decoders and fonts: ICO, AVIF, CFF/OpenType outlines, colour glyph tables, font registry and fallback, vertical text, @font-face descriptor logice.fmt.*, e.text.*Charts, UI toolkit, PDF. WOFF/WOFF2 already live in e.fmt.woff/woff2.
SVG, filters, gradientse.gfx.svg, e.gfx.filter (exist)Port Dart's SVG probes as vectors here.
Reader-mode extraction, accessibility tree rules, CSS animation timing and interpolatione.fmt.html.readable, e.fmt.html.accessible, e.ui.animationDiff against e.ui.animation first; extend it rather than duplicate.
Stays in the browser engine e.web.* (needs a browsing context)
Browsing contexts, event loop (HTML processing model), navigation, session history, bfcache, iframes, workers, service workerse.web.pageOrigins, task queues, lifecycle.
Host-object bindings: DOM, CSSOM, events, timers, fetch/XHR, storage, observers, Selection, forms and validation, permissionse.web.bindGlue between e.js, e.fmt.dom and the page.
Same-origin / CSP / CORS enforcement on a request made by a document; cookie and cache partitioning keyse.web.page calling e.net.*The policy math is in the library; deciding “whose request is this” is the browser’s.
contenteditable, editing commands, DevTools protocole.web.edit, e.web.devtoolsReuse e.ui text editing for the caret and IME.
Stays in the control e.ui.browser and the app
Widget glue: paint into widget.Custom, pointer/wheel/keys/IME routing, focus, scrolling and scrollbars, cursor, zoom, find bar, context menu, select popups, dialogs, tooltips, file pickere.ui.browserThin by rule; anything reusable is pushed down into e.ui.html_view or the library.
Tabs, omnibox, bookmarks, history, downloads, settings, importapp layer (NeperOS Browser, desktop shell)Not part of the control.
Drivers x.web.*
Renderer process, OS sandbox, site isolation, credential store, platform keychains, Chrome import decryptionx.web.*OS-specific.

Practical consequence for ordering: M1 can ship e.ui.html_view (static page) before the browser control exists, and every layer below it is usable by other apps the day it lands. Each new library module still needs its fenced surface in docs/module-apis.md, an entry in docs/modules.json and fixtures, per the repo rules.

Milestones

#MilestoneExit criteria (all measurable)
M1Static page in the controle.ui.browser loads an HTTP(S) page, no JS: block + inline layout, images, links, scrolling. Hacker News matches the Dart golden within pixel tolerance. html5lib runner exists. Covers P0, P1 (parse/style core), P2.1–2.3, P3.1–3.3, 4.1–4.2, 7.1.
M2Modern CSS layoutFloats, positioned, table, flex, grid, multicolumn, transforms, gradients, shadows. WPT CSS reftests ≥ 116/122 (Dart parity); layout pixel-probe suite carried over and green.
M3Real web without JSWikipedia (12k-node table), MDN (web fonts), craigslist, abv.bg match Dart goldens; SVG, filters, caching, CORS/CSP, cookies, HTTP/2, TLS interop with real servers; shell with tabs and history.
M4JavaScriptEngine passes the agreed test262 subset; DOM bindings complete enough for jQuery 3.5.1 init, AJAX re-render, timers, events, iframes; JS and layout feedback loop (getBoundingClientRect).
M5Dart parity and retirementEvery mig item done; html5lib at least 86% (target 100%); NeperOS browser.e replaced by the control (C159 closed); Dart repo archived with the oracle artifacts.
M6Beyond DartAll gap items: Shadow DOM and custom elements, Workers and Service Workers, contenteditable, process isolation and sandbox, DevTools, WebAssembly, vertical text, audio/video, SPA support.

Items

Track: mig = done in Dart (partly or fully), port it; gap = missing or unfinished in Dart, build in Neper; core = Neper language/library prerequisite. “Dart” column says where Dart stands. “Neper home” is the intended module or the existing module that already covers part of it. Size: S ≤ 1 day of agent work, M ≈ 2–4, L ≈ 1–2 weeks, XL = multi-week programme.

IDTrackItemStatusDartNeper home / notesSize

Test oracles

AreaOracleBaseline to match
HTML parserhtml5lib tree-construction: 58 .dat files, ~1791 casesDart 1549/1791 (86.5%) · Neper 1791/1791 (100%), floor enforced. The run used to abort outright: lookup_entity read text[at] with no bounds check, so a document ending in a bare & trapped and took the whole corpus with it (fixed 2026-11-07)
CSS reftestsWPT reftests (scripts/gen_wpt_manifest.py, checkout outside the repo), 800×600, match/mismatch pixel compare, Ahem fontDart 116/122 (95%) · Neper vendored corpus 190/212 and ~9,900 of 13,377 across the full WPT CSS checkout. See 9.2 for the per-suite split
DOM / web platformScript-side WPT (clone at D:/data/wpt-script) via tests/browser/web/run.edom/ 39,023 of 39,590 (98.57%) over 614 files, measured 2026-11-07. Must be chunked: ranges one file per process (2.7 GB each), the rest 8 per process in parallel, every process with a timeout
JavaScripttest262 upstream checkout (D:/wptdata/test262)40,458 of 40,938 (98.83%), measured 2026-11-07: language/ 99.97%, built-ins/ 97.95%
CSS unitsengine_core test files + layout fixtures (display-list geometry asserts)The css_* fixtures cover this; scripts/gen_*_test.py + reference pattern for anything new
Real sitesFrozen offline corpus + Dart PNG / node-dump goldens (HN, Wikipedia, MDN, craigslist, abv.bg)Pixel tolerance per site agreed in P0.3
Net policyCORS, CSP, cookies, cache, private-network vectors from engine_core/test/netExisting e.net.policy/ssrf differential (D2351) is the model
JS differential oraclesjQuery 3.5.1, QuickJS-ng as a differential referenceDart: QuickJS ES2023-ish ceiling, no Wasm. Neper's interpreter has no Wasm engine yet (5.17)
Performancebenchmarks/ harness: parse, style, layout, raster, JSNo Dart numbers recorded; P0.7 records them before retirement

Risks

RiskMitigation
JS engine size and speed (interpreter only, no JIT, own GC, UTF-16 strings on a UTF-8 language)Phase it (ES5 core → ES2015+ → async/modules); bytecode + shapes + inline caches from the start; record benchmarks against QuickJS early; test262 gates each stage.
Scene limits and CPU raster speed on full pagesTile by viewport, cull, glyph cache, damage regions (already in the scene); decide on the Vulkan backend by M3 measurements.
Layout fidelity depends on text metrics that move from dart:ui to e.textAhem in tests; port the pixel-probe harness first; accept per-site tolerance rather than bit-exact goldens.
e.fmt.html conformance unknown (no adoption agency, foster parenting or template yet)Closed 2026-11-07: the adoption agency, foster parenting, template modes, foreign content, quirks and fragment parsing are all in, and html5lib is 1791/1791 with the floor enforced. The corpus was silently unrunnable before this session because of a parser bounds trap, so check that it actually runs before trusting any older figure.
A red conformance number may be a harness limitation, not a bugOver 40 "failures" in dom/events turned out to be action_sequence() is not implemented by testdriver-vendor.js, and 30 more are an unimplemented WICG proposal. Before planning around a cluster, read one failure message: a chunk that reports 0 subtests is usually a harness status, and a family that fails uniformly is usually infrastructure.
Long conformance runs fail in ways that look like slow progressOne web-run process cannot measure dom/ (it dies of an access violation after ~3,500 CPU-seconds); a single hung test holds a parallel worker and the batch then stalls with no output, which is indistinguishable from "still running". Chunk by memory need, run in parallel, and give every process a timeout.
No TLS 1.2, HTTP/2 only as codecs, no HTTP/3 transportTLS 1.2 and HTTP/2 transport are P4 prerequisites for M3; HTTP/3 deferred.
Neper repo discipline: one queue item at a time, shared working tree, Windows suite never green end to end, no closures, no name shadowingEach phase maps to queue items with acceptance lists; callbacks use fn pointer + context; add fixtures to run.ps1 / run.sh per module; keep module surfaces in docs/module-apis.md and docs/modules.json.
Single window per e.ui.appCore item N.8; until then shell multi-window = one process per window.

How to update this page