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
| Phase | Scope | Progress | Items |
|---|
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)
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.gfx.scene.Builder.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.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.paintlowers the latest immutable display list toscene.Builder. Hand-off throughe.channel, double-buffered arenas, tiled to the scene limits. - Net runs on
e.taskpool; 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
RenderTreeSnapshotprotocol (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)
| # | Decision | Rationale / consequence |
|---|---|---|
| K1 | Three 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. |
| K2 | Write 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). |
| K3 | All 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. |
| K4 | Memory: 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. |
| K5 | Paint = 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"). |
| K6 | Freeze 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. |
| K7 | Do 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. |
| K8 | Scope 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. |
| K9 | This 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”.
| Piece | Home | Other users and notes |
|---|---|---|
| General-purpose library (outside the browser) | ||
| HTML tokenizer, tree builder, serializer, sanitizer, accessibility | e.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-walkers | e.fmt.dom (new) | Scraping, tests, XSLT/XPath, mail, server-side rendering. No events, no scripts, no window. |
| CSS syntax, selectors, cascade kernels, container/scope, values | e.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 values | e.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 testing | e.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 tiles | e.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 subset | e.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, interpreter | e.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, proxy | e.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 logic | e.fmt.*, e.text.* | Charts, UI toolkit, PDF. WOFF/WOFF2 already live in e.fmt.woff/woff2. |
| SVG, filters, gradients | e.gfx.svg, e.gfx.filter (exist) | Port Dart's SVG probes as vectors here. |
| Reader-mode extraction, accessibility tree rules, CSS animation timing and interpolation | e.fmt.html.readable, e.fmt.html.accessible, e.ui.animation | Diff 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 workers | e.web.page | Origins, task queues, lifecycle. |
| Host-object bindings: DOM, CSSOM, events, timers, fetch/XHR, storage, observers, Selection, forms and validation, permissions | e.web.bind | Glue 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 keys | e.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 protocol | e.web.edit, e.web.devtools | Reuse 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 picker | e.ui.browser | Thin by rule; anything reusable is pushed down into e.ui.html_view or the library. |
| Tabs, omnibox, bookmarks, history, downloads, settings, import | app 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 decryption | x.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
| # | Milestone | Exit criteria (all measurable) |
|---|---|---|
| M1 | Static page in the control | e.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. |
| M2 | Modern CSS layout | Floats, positioned, table, flex, grid, multicolumn, transforms, gradients, shadows. WPT CSS reftests ≥ 116/122 (Dart parity); layout pixel-probe suite carried over and green. |
| M3 | Real web without JS | Wikipedia (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. |
| M4 | JavaScript | Engine 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). |
| M5 | Dart parity and retirement | Every 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. |
| M6 | Beyond Dart | All 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.
| ID | Track | Item | Status | Dart | Neper home / notes | Size |
|---|
Test oracles
| Area | Oracle | Baseline to match |
|---|---|---|
| HTML parser | html5lib tree-construction: 58 .dat files, ~1791 cases | Dart 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 reftests | WPT reftests (scripts/gen_wpt_manifest.py, checkout outside the repo), 800×600, match/mismatch pixel compare, Ahem font | Dart 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 platform | Script-side WPT (clone at D:/data/wpt-script) via tests/browser/web/run.e | dom/ 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 |
| JavaScript | test262 upstream checkout (D:/wptdata/test262) | 40,458 of 40,938 (98.83%), measured 2026-11-07: language/ 99.97%, built-ins/ 97.95% |
| CSS units | engine_core test files + layout fixtures (display-list geometry asserts) | The css_* fixtures cover this; scripts/gen_*_test.py + reference pattern for anything new |
| Real sites | Frozen offline corpus + Dart PNG / node-dump goldens (HN, Wikipedia, MDN, craigslist, abv.bg) | Pixel tolerance per site agreed in P0.3 |
| Net policy | CORS, CSP, cookies, cache, private-network vectors from engine_core/test/net | Existing e.net.policy/ssrf differential (D2351) is the model |
| JS differential oracles | jQuery 3.5.1, QuickJS-ng as a differential reference | Dart: QuickJS ES2023-ish ceiling, no Wasm. Neper's interpreter has no Wasm engine yet (5.17) |
| Performance | benchmarks/ harness: parse, style, layout, raster, JS | No Dart numbers recorded; P0.7 records them before retirement |
Risks
| Risk | Mitigation |
|---|---|
| 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 pages | Tile 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.text | Ahem 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 bug | Over 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 progress | One 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 transport | TLS 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 shadowing | Each 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.app | Core item N.8; until then shell multi-window = one process per window. |
How to update this page
- Edit the
ITEMSarray below: change a status and, when closing, put the commit or D-number in the notes. No generator; the page recomputes everything on load. - Status meanings: done verified against the named oracle; partial code exists in Neper but is unverified or incomplete; wip a session is on it; todo; blocked waits on another item; deferred deliberately out of the programme.
- New work discovered while porting is appended as a new row, never silently folded into an old one.