This repository has no description
bez roadmap.md
27 kB
Markdown

Roadmap #

Living document. Phases are ordered by what can produce a verification signal, not by architectural tidiness.

Layering #

Three categories, only one of which is generated.

Generated from specs — components whose specifications are written as imperative algorithms:

  • HTML tokenizer and tree construction (an explicit state machine in prose)
  • Layout algorithms (block, inline, flex, grid, table)
  • DOM method and property semantics

Linked, never generated — real engineering that lives outside browser-specs:

  • JS engine (candidate: Boa, or bindings to an existing engine)
  • Text shaping (HarfBuzz), Unicode (ICU), font parsing
  • GPU rasterization (wgpu / vello)

Linked for now, a generation target later — components that exist as mature Rust and whose co-generation would destroy attribution in the probe:

  • CSS parsing, selector matching, cascade and computed values, via Stylo

Stylo's standalone usability is confirmed rather than assumed: it publishes from its own repository (servo/stylo, version 0.21.0 as of September 2026) with roughly 187k recent downloads and thirty reverse dependencies, of which the majority are not Servo — Blitz, micropdf, fulgur, kozan-core, solara, tauri-runtime-blitz. The default cargo feature is servo; the alternative is gecko.

Integration requires implementing Stylo's TElement, TNode and TDocument traits over the local DOM. Blitz's implementation at blitz-dom/src/stylo.rs is roughly 55KB plus a small device and adapter set — on the order of 1,500 to 2,500 lines, Apache-2.0, and the practical documentation, since the README carries no examples and the prose docs are an architecture post and an out-of-date book chapter. There is no API stability policy and reverse dependencies are spread across six major versions, so the version is pinned.

Stylo is MPL-2.0: file-level copyleft, so linking is unencumbered but modifications to Stylo's own files must be published. This matters only once engines ship.

Hand-written once — the substrate specs assume but never define:

  • Box tree, fragment tree, computed style storage
  • Invalidation and dirty tracking
  • Event loop and task queues

The substrate is the foundation every generated algorithm sits on. Errors there invalidate all generated code above it, so it is not a generation target.

Verification strategy #

A real browser is the training signal; WPT is the scorecard.

Headless Chrome driven over CDP walks the DOM and dumps per-node geometry (getBoundingClientRect) and computed styles as JSON. The engine under test emits the same JSON for the same input. The diff is numeric and per-node, in tree order.

Why this rather than WPT as the primary loop:

  • No rasterizer required, and no JS engine required inside the engine under test — Chrome runs the page's JS to produce the dump.
  • Unlimited labeled data from arbitrary or fuzzed input, rather than a fixed corpus.
  • Attribution: the first node in tree order whose geometry diverges names the algorithm that is wrong. A failing reftest names nothing.

WPT remains the external, honest measure of conformance, reported but not optimized against directly.

Geometry mapping #

What the oracle compares against, and therefore what the fragment tree must be able to produce. From CSSOM View:

  • getClientRects() returns one rect per box fragment, in content order, describing its border area, including fragments of zero width or height, with the transforms of the element and its ancestors applied.
  • Anonymous block boxes are replaced by their child boxes, repeatedly, until none remain. They never produce rects of their own.
  • For display: table and inline-table, the table box and caption box are included but the anonymous container box is not.
  • getBoundingClientRect() returns a zero rect for an empty list; the first rect if all rects have zero width or height; otherwise the smallest rect containing every rect of non-zero width or height.

Requirements this places on the fragment tree:

  • A fragment carries its border-box size, its offset relative to its parent fragment, and an optional originating node id. Anonymous fragments carry none and are flattened away when rects are produced.
  • Offsets are parent-relative, never absolute. This is forced by the pure-function contract — a layout algorithm is invoked without knowing where on the page its result will sit — and matches LayoutNG.
  • Layout computes in logical coordinates; fragments store physical ones. The conversion happens at fragment construction, where the containing block's size and writing mode are both known.
  • Viewport resolution is a separate walk that accumulates offsets, relative-position shifts, scroll offsets and a transform matrix. Under a transform the rect is the axis-aligned bounding box of the transformed border-box quad.

Consequences for the harness:

  • Compare fragment sizes and parent-relative offsets, not absolute rects, reconstructing relative offsets from Chrome's absolute ones by subtracting the parent's. One wrong offset then shows up as one wrong value instead of corrupting every descendant, which is what makes a diff attributable.
  • Collect getClientRects(), not only getBoundingClientRect(). The two coincide for Phase 0 block boxes, but collecting the list means the corpus survives the arrival of inline layout.
  • Collect a computed-style subset alongside geometry. A mismatch there indicts the Stylo integration rather than generated layout — a second attribution axis for free.
  • Correlate nodes by document-order index, with the sequence of tag and depth as a checksum. A shape mismatch is its own attributable failure: parser divergence, not layout.

Precision. Blink computes layout in LayoutUnit, a 1/64 px fixed-point type that quantizes at every operation. Matching it exactly would mean replicating where Blink rounds, which is internal to Blink and absent from any spec, and which generated code will not reproduce by accident. So: fixed-point arithmetic internally for determinism, pass/fail at a tolerance of roughly 1/64 px, and the raw numeric delta as the reward signal, which needs no threshold at all.

Confounds to eliminate before any measurement is meaningful:

  • UA stylesheet. Chrome's defaults — body margin, per-element display types — would otherwise differ from the engine's on every single node. Phase 0 test inputs carry an explicit reset; a real UA stylesheet is its own work item.
  • Scrollbars. Overflowing content gives Chrome a scrollbar that steals viewport width by a platform-dependent amount. The viewport is fixed through CDP device metrics and Phase 0 content is kept non-overflowing.
  • Device pixel ratio and zoom are both pinned to 1.

Layout invalidation #

The purity decision already answers this. Because layout is fn(box, constraint_space) -> fragment, invalidation is not an algorithm to design — it is a cache key. Dirty-bit propagation is what an engine needs when layout mutates shared state; memoization is correct by construction when it does not. This is why LayoutNG defines a constraint space at all: the constraint space is the cache key.

The catch is completeness. Every input an algorithm reads must be in the constraint space, or the cache is unsound and fails silently. Purity is therefore load-bearing rather than stylistic, and is enforced by the signature: a layout function receives the box's style, a constraint space, and a child-layout callback, and has access to nothing else.

Constraint space contents at Phase 0 scope:

  • available inline size
  • block size, definite or indefinite
  • percentage resolution basis
  • incoming collapsible margin — margin collapsing reaches outside the box, which is precisely what Phase 0 exercises
  • whether the box participates in or establishes a block formatting context
  • writing mode and direction

Intrinsic sizing (min-content, max-content) is a separate query over the same box with a different constraint space, cached separately.

Out-of-flow boxes break tree-shaped purity, since their containing block is an ancestor rather than the parent. LayoutNG propagates them upward to the containing block and lays them out in a later pass. They are excluded from Phase 0, but the fragment type carries a slot for propagated out-of-flow descendants so that adding them is not a rewrite.

Cold-versus-warm equality is a free oracle. Laying a document out twice — once with an empty cache, once warm, and again with a randomized subset of subtrees cached — must produce identical fragments. Any mismatch proves a hidden input, meaning an impure algorithm. This runs with no browser, costs almost nothing, and targets exactly the failure mode generated code exhibits. Even before caching exists, calling each layout function twice on identical inputs catches nondeterminism from sources like hash iteration order.

Phase 0 implements no caching. It defines the constraint space completely, enforces the pure signature, and runs the determinism check from the first day.

Oracles #

Three engines rather than two, driven through one Playwright installation.

Two oracles can only detect disagreement and discard the case. Three yield a majority: a 2–1 split keeps the case with the outlier identified, and a three-way split marks genuine underspecification. Disagreement stops being lost data and becomes a graded reward.

Operationally three oracles cost about what one does, since Playwright drives Chromium, Firefox and WebKit through a single API. Corpus building is one-time and cached, so the overhead relative to raw CDP does not matter.

Two things to know:

  • Playwright's WebKit is a patched build, not Safari, and diverges on platform-dependent behavior such as fonts and scrollbars. Adequate for block geometry under a reset stylesheet; not evidence about text.
  • Quantization differs per engine — Blink and WebKit use 1/64 px, Gecko uses app units at 1/60 px. Comparison tolerance accommodates the coarsest.

Browser compat data as a prior #

Measured rather than assumed, against the full BCD dataset: 17,663 notes exist, but 11,470 are api and most of the remainder are historical. Restricting to CSS, currently supported, and non-historical leaves 238 notes and 64 partial_implementation flags.

The surviving slice is genuinely high quality — specific, and frequently carrying bug tracker links that explain far more than the note itself. It is also concentrated in newer and edge features (at-rules, flexbox baselines, multicol, backgrounds) and barely touches Phase 0 block layout.

So its use is as a prior on where the three-way oracle vote will split, known before anything runs: which features are contested, which cases should be pre-marked as untrustworthy reward, and where search budget would be wasted because no engine is right. That makes it a Phase 3 asset rather than a Phase 0 input. The machine-readable partial_implementation flag is more directly useful than the prose.

Test input distribution #

The corpus is the reward function. Two ways it can mislead, with different fixes.

Matching Chrome is not implementing the spec. A single-oracle loop produces an engine that reproduces Chrome, including its bugs and its deviations. Mitigations, in order of cost: WPT stays an independent exam that is never trained against; a second oracle (Firefox) runs over the same corpus, and cases where the two browsers disagree are marked as untrustworthy reward and excluded or down-weighted rather than optimized. Cross-browser disagreement is a useful signal in its own right — it localizes spec ambiguity without anyone reading the spec.

Random values do not exercise an algorithm. Most randomly generated boxes are trivially satisfied and discriminate nothing. Generation is stratified by the spec's own case analysis, since layout specs are explicit case enumerations. For Phase 0 that means sampling deliberately across:

  • the width equation's branches — auto width, auto margins on one or both sides, over-constrained, and which value is dropped under each direction
  • shrink-to-fit versus available-width fill
  • min/max clamping, including min greater than max, and the re-resolution that clamping forces
  • percentage resolution against a containing block, including percentage heights against an auto-height parent
  • margin collapsing in all its forms: adjacent siblings, parent with first or last child, self-collapsing empty blocks, collapse-through chains, negative margins mixing with positive, and block formatting context roots that stop collapsing entirely
  • box-sizing, and padding or border interacting with percentage widths
  • nesting depth, since collapse-through and percentage chains only appear in deep trees

Values are chosen against boundaries and fractions rather than round numbers: zero, negative, 33.333%, thirds of a container, quantities that fall between 1/64 px steps. Round numbers hide exactly the rounding bugs the fixed-point arithmetic exists to make deterministic.

Three corpora, with different roles:

  • An enumerated set built from the case analysis above — deterministic, reproducible, the development signal.
  • A grammar-generated random set on rotating seeds, regenerated periodically so it cannot be overfit, held out from the repair loop.
  • WPT, the independent exam, never used as a training signal.

Shrinking belongs in the harness from day one. A failure found on a large document must be delta-debugged to a minimal reproducer before it reaches the repair loop. The quality of the repair signal is roughly the inverse of the size of the failing input; a three-line case tells a generator what is wrong, a two-hundred-node document does not.

Failure-weighted sampling, with a permanent regression set. Oversampling the regions where the engine currently fails is cheap given a fast verifier, but concentrates the distribution and invites forgetting. Every case that has ever passed stays in the suite permanently, so the loop cannot trade old correctness for new.

Coverage has two measures. Spec-case coverage holds by construction, since the generator is keyed to the case analysis. Branch coverage of the generated Rust is the second, and it is read as a hallucination detector rather than a coverage metric: a generated branch no corpus case can reach is either dead code the model invented or a genuine gap in the corpus, and both are worth knowing.

Real-world pages are excluded until Phase 3. They exercise everything simultaneously, so failures are unattributable, and they require text, fonts, images and network — all of which Phase 0 deliberately omits. Every case is seeded and the seed stored, so any failure replays exactly.

Substrate contract #

The interface generated code is written against. Four rules.

Generated code is pure. An algorithm receives computed style, available space, and a callback to lay out a child; it returns fragments. It does not walk the DOM, mutate shared state, set dirty bits, or reach sideways. Only hand-written substrate mutates. This mirrors how specs are written ("let X be…, return Y") and makes every generated algorithm unit-testable from a hand-built input struct, with no engine present.

The substrate speaks spec vocabulary. Where the spec says "inline size", the accessor is inline_size. Every gap between spec nouns and substrate nouns is a translation step where generation can guess wrong.

Illegal states are unrepresentable, so the compiler is the first oracle. Computed, used, and actual values are distinct types; auto is a variant, not a sentinel; a percentage cannot be resolved without an explicit basis. A generated algorithm that confuses a computed value for a used value fails to compile. Compile errors are the cheapest repair signal available — no harness run needed.

Logical axes from day one. Inline/block, not horizontal/vertical, throughout. Physical coordinates appear only at paint. CSS specs have been written in logical terms since Writing Modes; retrofitting logical axes onto a physical engine is a known multi-year cost in existing engines.

DOM layer, shaped by Stylo #

Stylo constrains the layer above computed style and nothing below it. The seam is ComputedValues: the DOM and style side is largely dictated, the box tree, fragment tree and value types remain purpose-built. Requirements, read off Blitz's trait impls:

  • Nodes live in an arena keyed by integer id. The Stylo-facing type is a small Copy handle pairing a document reference with that id; all tree navigation goes through the arena. Stylo copies, hashes and compares these handles freely.
  • One handle type serves as ConcreteNode, ConcreteElement, ConcreteDocument and ConcreteShadowRoot.
  • Every element hosts a Stylo-owned style::data::ElementData slot behind interior mutability, reached through ensure_data / borrow_data / mutate_data / clear_data. Computed style lands here. The unsafe contract is that traversal holds exclusive access.
  • Nodes carry dirty-descendant bits under an invariant Stylo depends on: a set bit implies every ancestor is set. The style half of invalidation is therefore not a free design choice.
  • Ids, class names and attribute names are interned atoms; elements carry ElementState bits for :hover, :focus, :checked and similar.
  • Ambient machinery to own: Stylist, SharedRwLock and guards, SnapshotMap, Device, a per-pass SharedStyleContext, and a DomTraversal impl.

Two operational notes. Stylo asserts global thread state, so every verifier worker thread calls style::thread_state::enter(ThreadState::LAYOUT) — otherwise parallel search fails as an obscure panic. Blitz leaves store_children_to_process and did_process_child unimplemented, running traversal sequentially, which independently matches the decision not to design for parallelism.

Phase 0 economies, most of them already taken by Blitz: shadow DOM left as todo!(), is_html_document hardcoded true, quirks mode fixed to none, no MathML or SVG, animations stubbed.

Unverified: whether Stylo's atoms and html5ever's LocalName are the same interning type or need conversion at the boundary. Blitz's use of the local_name!() macro suggests they align, but this is inference and settles cheaply once a crate compiles.

Model strategy #

Fine-tuning is not the starting point, for two reasons: there is no training data until a verifier exists, and browser-specs changes continuously while a fine-tune goes stale.

Ordered by cost, each step justified only by the previous one failing:

  1. Context engineering — spec section, substrate API surface, sibling implementations
  2. Retrieval over browser-specs / BCD / web-features, which also stays current
  3. Best-of-N with the verifier selecting
  4. Supervised fine-tuning on verified generations — the engine's own passing output
  5. RL against the geometry diff, which is a dense per-node numeric reward rather than pass/fail

Every step past the first requires the verifier. The verifier is the asset; the model is replaceable.

Step 3 is the baseline that justifies steps 4 and 5, and it is measured before any training infrastructure is built. A sufficiently large N against a good verifier may cover the Phase 0 surface outright.

Step 5 is the one part of this project that is novel as research rather than engineering. Web layout is an unusually clean verifiable-reward domain: tasks are generated without limit, ground truth comes from an existing oracle with no human labeling, and the reward is dense per-node numeric error rather than a binary pass.

Compute shape #

Available hardware is GPU-heavy with a few hundred CPU cores. Generation is therefore abundant and verification is the scarce side, which makes the search loop a throughput problem. Two constraints follow, both of them harness design rather than later optimization:

Ground truth is cached, never recomputed. A given input has one correct geometry permanently. CPU is spent once building a large (input, expected geometry) corpus from pooled Chrome instances; the inner loop then runs only the engine under test against cached JSON, in under a millisecond, with no browser involved. Headless geometry dumps need no GPU, so corpus building does not contend with model serving.

Candidates are compiled in batch. N samples become N functions in one compilation unit and a single cargo check, not N invocations. Normalized-AST deduplication runs first, since high-temperature sampling yields many alpha-equivalent duplicates.

Verifier evaluations per second is the metric the harness is designed around. An oracle that spawns a process per test costs roughly 100x an oracle with pooled, reused sessions, and that ratio is the search budget.

Phases #

Phase 0 — Feasibility probe #

Answer one question: does spec prose convert to Rust that passes against a real browser oracle, and at what first-shot rate?

  • Build the CDP geometry harness first, before any generation
  • Minimal substrate: DOM tree via html5ever, computed style via Stylo, no JS
  • Scope: block boxes with no text content — sizes, margins, padding, borders, margin collapsing. Inline layout, line breaking, bidi and shaping are excluded deliberately
  • Generate only CSS 2.1 normal-flow sizing within that scope
  • Score against Chrome geometry dumps; report WPT css/CSS2/normal-flow separately

Exit criteria: measured first-shot rate, rate after N repair iterations, and a failure taxonomy (spec misreading, substrate mismatch, missing prerequisite, harness gap).

Phase 1 — Substrate design #

Hand-written core that generated code targets, built to the contract above.

Phase 2 — Generation pipeline #

Spec text in, Rust plus tests out. Repair loop driven by compile errors first, then geometry diff.

Phase 3 — Breadth #

Expand the generated surface. Module boundaries evaluated here, not assumed earlier.

Phase 4 — Configurable engine builds #

Include/exclude web surface at build time. Separable seams identified so far: globals-attached APIs (WebRTC, WebGL, IndexedDB, Web Audio) and formatting contexts behind a dispatch trait. "Tree-shake the engine for one app" is downstream of this and remains speculative.

Decisions #

Recorded as facts with their reasons.

  • Generation granularity is one whole algorithm per spec section, with spec steps as numbered comments above their implementation. The unit of generation matches the unit of verification; a single spec step has no oracle and cannot be tested in isolation.
  • A real browser, not WPT, is the primary verification loop. WPT gives pass/fail over a fixed corpus with no attribution; a geometry diff gives per-node numeric error over unlimited input.
  • Box tree and fragment tree are separate. A box can produce many fragments (an inline box across three lines is one box, three fragments), so fragmentation cannot be expressed in a single tree. The split also makes layout a pure function: box tree in, immutable fragment tree out. Merging the two is a rewrite that existing engines have each paid for once. Every production engine began with a mutable layout tree and has migrated away from it; none has migrated toward one.
  • Stylo provides style; the cascade is not generated in early phases. It is mature production Rust — written for Servo, then adopted into Firefox to replace a fifteen- year-old C++ style system. Co-generating cascade and layout would make failures unattributable between the two, which is the one thing the probe must avoid.
  • Generated code is spec-literal and slow. Optimized code is both harder to generate and harder to verify, and the two compound. The naive implementation also becomes a differential oracle for any optimized version written later — a reference layer no existing engine has, because none kept a slow-but-correct version around.
  • Layout is not designed for parallelism. Servo's parallel-layout bet did not repay its complexity; the parallelism win was in style resolution, which is per-element and comes free with Stylo. Layout dependencies are sequential.
  • Blitz contributes its Stylo glue only, not its substrate. Adapting blitz-dom/src/stylo.rs avoids rediscovering how Stylo's traits are satisfied, while the box tree, fragment tree and value types stay purpose-built. Blitz's layout side is physical-coordinate f32 with no computed/used distinction, which contradicts the substrate contract and is the part being replaced regardless.
  • Text is excluded from Phase 0. Inline layout, line breaking, bidi and shaping are where every engine's complexity concentrates and where spec prose is least algorithmic. Block-box geometry is where the prose is most algorithmic.

Open questions #

Gating Phase 0:

  • Whether Stylo's atoms and html5ever's LocalName interoperate directly.

Substrate:

  • How computed style is stored behind the flat spec-named accessors: grouped, shared and refcounted is the convergent answer, but this only matters once Stylo is replaced.

Later phases:

  • How much of WPT is reachable without JS, measured rather than guessed.
  • Whether IDL-driven bindings are ordinary deterministic codegen rather than generation, and where that line falls.

Inputs #

Prior art to study #

  • Blink / LayoutNG — constraint space in, immutable physical fragment out. The closest existing template for the pure-function layout contract, and the product of a multi-year migration away from a mutable layout tree.
  • Gecko — fragmentation via continuations, which creates frames in the persistent tree during layout and makes layout non-reentrant. The concrete cost of not splitting box and fragment trees.
  • Servo — the durable output was the crates with clean data-in/data-out interfaces (html5ever, selectors, cssparser, Stylo, WebRender), not the crate graph itself. That is the real modularity lesson.
  • Ladybird — spec steps transcribed as numbered comments directly above their implementation; evidence that spec prose is mechanical enough to transcribe. Its accelerants were the absence of users, back-compat obligations and performance constraints, which kept the code direct and obvious.
  • Blitz — html5ever, Stylo for cascade, Taffy for layout, Vello for paint, with a stylo_taffy crate translating Stylo's ComputedValues into the layout engine's style type. The same handoff boundary Genera needs, already load-bearing in an active project; the difference is that Genera generates the layout side.
  • Style struct storage — Gecko's style structs, Blink's copy-on-write ComputedStyle sub-structs and Stylo's Arc-shared structs are the same design reached independently three times, including the inherited/reset split. The answer if the cascade is ever generated.