Generated functions #
A generated function is one layout rule pulled out of the hand-written engine into a plain-data
function: typed inputs in, typed outputs out, no access to the DOM or Stylo. The
hand-written code around it (crates/layout/src/fragment.rs) decides when the rule
applies and hands it inputs already reduced to one decision; the generated function
decides what the answer is. Because the function is that small, a model can write it, and the harness can
score it alone with every other generated function held fixed. How one is generated and admitted is
in generation-loop.md; how its output is checked is in
geometry-verification.md.
flowchart LR
fragment["fragment.rs<br/>(hand-written walk)"] -- "plain inputs" --> gen["generated function"]
gen -- "plain outputs" --> fragment
fragment --> tree["fragment tree"]
The fifteen generated functions #
Scores are on each function's own corpus, as recorded in the committed candidate's provenance
record under crates/layout/src/generated/. The recipe corpus is 227 cases; the other
corpora are per-function.
| function | rule (CSS 2.1 unless noted) | committed candidate | score | how it was built |
|---|---|---|---|---|
| width | 10.3.3 width equation, 10.4 min/max | qwen3.8-flash-next |
140/227 | hand fixture first |
| margin | 8.3.1 margin collapsing | qwen3.8-27b |
156/227 | hand fixture first |
| percentage | 10.2 / 10.5 / 8.4 percentages | qwen3.8-27b |
116/227 | hand fixture first |
| absolute | 9.3, 10.3.7, 10.6.4 absolute positioning | qwen3.8-flash-next |
82/108 | model-first |
| float | 9.5, 9.5.1, 9.5.2 floats and clearance | claude-code:default |
109/109 | model-first |
| relpos | 9.4.3 relative positioning | claude-code:default |
104/104 | model-first |
| stacking | 9.9.1, Appendix E.2, CSS Color 3 opacity | claude-code:default |
105/105 | model-first |
| height | 10.6.3 / 10.6.7 auto height | hand-written | 105/105 | extracted from working code |
| overflow | scrollbar presence (CSSOM View) | qwen3.8-27b-fast |
105/105 | model-first |
| containing block | 10.1 which box is a box's containing block | qwen3.8-flash-next |
106/106 | model-first |
| transform | CSS Transforms 1 §3/§4/§5, the 2D transformation matrix | qwen3.8-27b-fast |
110/110 | model-first |
| inline atomic | 9.4.2, 10.8, 10.8.1 line boxes over atomic inlines | qwen3.8-flash-next |
111/111 | model-first |
| table | 17.5.2.1 fixed table layout, 17.5.3 row heights | hand-written baseline | 0/0 | no batch drawn yet |
| aspect ratio | CSS Box Sizing 4 §4, aspect-ratio and its size transfers |
qwen3.8-27b-fast |
33/33 | model-first, repaired |
| box generation | CSS Display 3 §2.5 box generation, CSS 2.1 9.7 display/position/float |
qwen3.8-27b-fast-2 |
48/48 | model-first, admitted by a feature job |
On the recipe corpus a function scores only the cases its own rule decides, so width, margin
and percentage each fall short of 227 alone; run together in layout() they pass 227/227
(roadmap.md → "Proof of concept", claim 1).
The containing-block function is the one whose answer is a choice of box rather than a
number: it names which rect the absolute function's equations are then solved against, which
is what position: fixed needed in order to exist at all.
The transform function is the one whose answer is a matrix, and the only one so far not drawn from CSS 2.1.
The inline-atomic function is the one whose answer is a set of positions rather than a single value, and the first to place more than one box per call: everything before it decided one box's geometry at a time.
Which of these the engine's real entry points actually call is traced in roadmap.md →
"Which resolver each generated function runs".
Per function #
Each function starts from a baseline candidate that reproduces the hand-written rule it
replaced, then model batches scored against browser-measured cases. Most functions here
were built with the older standalone *_generation_loop binaries, which
also take two or more seeded-wrong fixtures (deliberately broken candidates the gate
must reject). The job pipeline scores candidates against the browser-measured cases alone,
with neither seeded-wrong fixtures nor a hand-written reference implementation (docs/generation-loop.md step 2 gives the
reason).
Width (crates/layout/src/width.rs). Hand correct fixture 112/227; two seeded-wrong
fixtures 0/227. The first Claude batch compiled 0 of 5 probed (missing imports, redefined
types, and a fence-extraction bug); after fixing the prompt and extraction, a second batch
compiled 8/8, all 132/227 — above the fixture because the model also implemented 10.4
min/max clamping. Qwen 27B batches mostly failed to compile on an invented
LengthPercentageOrAuto::Percentage variant until one prompt sentence forbade it (1/8 →
6/8 compiling, winners 119/227). GLM-5.3 Flash compiled 0/8 (its analysis and code share
one reply field) and was replaced. A 32-candidate qwen3.8-flash-next batch across four
replicas compiled 19/32, admitted 14, and its best, 140/227, replaced the committed Claude
candidate — the first free-hardware winner to beat a paid one.
Margin collapsing (crates/layout/src/margin.rs). Two functions, not one: classify
(does this box's margin adjoin its neighbours?) and combine (the arithmetic for one
adjoining group). Admission also requires agreement with the crates/logic Datalog rules
box by box, because a candidate can reach correct geometry by coincidence
(docs/margin-collapse-experiment.md → "Verdict"). Hand fixture 156/227; one seeded-wrong
fixture (ignores_self_collapsing, 138/227, zero regressions) was rejected only by the
rules check. The first Qwen batch's five 156/227 candidates were all rejected by the rules
for gating self_collapsing on !establishes_bfc; one clarifying prompt sentence got half
the next batch through.
Percentage resolution (crates/layout/src/percentage.rs). Hand fixture 116/227;
8 of 8 Qwen candidates matched it on the first batch. Limitation carried forward: a
percentage-only mistake cannot regress anything while the baseline fails every percentage
case, so the geometry gate cannot reject it.
Absolute positioning (crates/layout/src/absolute.rs), the first model-first generated function.
108 cases, twelve categories. Two Claude batches plateaued at 74/108; a third was refused
for billing. A 32-wide qwen3.8-flash-next batch closed OverConstrainedRtl (0/8 → 8/8)
and its candidate replaced Claude's at 82/108. Still at 0%: BottomHeightTopAuto,
OffsetsDefineSize, PercentageOffsetsAndSizes. Cost comparison:
docs/model-first-cost.md.
Floats (crates/layout/src/float.rs). The structural half (float lists, the second
layout pass, shrink-to-fit width) is hand-written; the generated function is the placement rule. The
regression set is seeded from every fixture's passes (97 cases) because a baseline that
ignores clear could not otherwise reject a candidate that also ignores it. One
claude-code batch: 8/8 compiled, all 109/109. No hand-written candidate exists.
Relative positioning (crates/layout/src/relpos.rs). One claude-code batch reached
104/104. Its corpus was the first to depend on two other generated functions' real behaviour, which
exposed three harness bugs before anything could be scored honestly.
Stacking order (crates/layout/src/stacking.rs), the first generated function whose
output is an ordering. Needed a new oracle (document.elementsFromPoint, exact three-way match,
docs/dump-format.md → "Stacking probe") and two extra corpus categories before the
seeded-wrong fixtures could be rejected. One claude-code batch: 8/8 at 105/105.
Block height (crates/layout/src/height.rs), extracted from working hand-written
code with no behaviour change. All 8 claude-code candidates matched it at 105/105, none
exceeded it, so the hand-written rule stays. Its first loop run scored the known-correct
baseline at 93/105, which exposed a harness bug (margin collapsing held at its baseline).
Overflow (crates/layout/src/overflow.rs). Decides whether a box shows a scrollbar.
A first candidate reached 90/105 (right for boxes that clip, wrong for visible spill);
a mixed batch from two gateway models produced a 105/105 candidate
(docs/model-first-cost.md → "Overflow: a mixed batch").
Aspect ratio (crates/layout/src/aspect_ratio.rs). Gives a box with aspect-ratio
the content size the ratio implies, with min and max sizes transferred through it. Its
cases are existing WPT pages (oracle/suites/features/aspect-ratio.json) measured by the
browsers, 33 train and 7 held out, and its prompt shows each case as the function's inputs
and expected outputs. Of a batch of 8 first replies from qwen3.8-27b-fast, 5 did not
compile and the best passed 25 of 33; one round of bench/repair-loop.sh (8 repair
requests) gave the committed candidate, 33 of 33 train and 7 of 7 held-out. Admitted on
passing every train case, with the held-out score recorded and not gating. Its provenance
record's definition_closure_hash is the SHA-256 of the three referenced CSS 2.1
sections' text as the prompt cites them, and prompt_hash that of the first prompt,
before the repair conversation.
Containing block (crates/layout/src/containing_block.rs), the generated function
that made position: fixed possible. Its answer is a choice of box rather than a number:
which rect CSS 2.1 10.3.7/10.6.4's equations are then solved against. Before it,
fragment.rs decided that itself with one line — the tree root, or the nearest ancestor
whose position is not static — which has no way to say "the viewport", so fixed was
flattened to static in dom::computed::position and a fixed box was laid out in normal
flow. Over the corpus as it stands (106 cases): baseline 36/106, seeded-wrong fixtures
37/106 and 71/106, both rejected on 13 regressions each, committed candidate 106/106. One
batch across four gateway models compiled 77 of 82 candidates, and every candidate that
scored reached 105/105 — the whole corpus at that batch, one recipe smaller — but one. The
loop stopped because no failing case was left, not on a plateau.
The corpus had to be built against a trap: the oracle never scrolls, so at scroll 0 a fixed box and a box against the initial containing block land in the same place. Every fixed recipe but the deliberately easy top-level one either sits inside a positioned ancestor it has to escape or measures against the viewport's own 600 px height, which no ancestor box in these documents is tall enough to fake.
The recipe added after the batch is the one that separates a shift from a position: its
relative ancestor is moved with top/left rather than with margin, so CSS 2.1 9.4.3
moves the ancestor's fragment without moving its in-flow position. An escaping fixed box
must not be dragged along by that, while an absolutely positioned sibling must move by
exactly it — two demands on the same arithmetic that a corpus of margin-moved ancestors
cannot tell apart.
Transform (crates/layout/src/transform.rs), the first function drawn from a spec
other than CSS 2.1 and the first whose answer is a matrix: a box's transform list,
transform-origin and border-box size in, the six components of an affine matrix out. The
structural half is hand-written — fragment.rs composes each box's matrix with every
ancestor's and reports the bounding box of the transformed border box, which is what
getBoundingClientRect() returns. Normal flow is untouched by design: CSS Transforms 1
says the transform does not affect the flow, so one corpus category puts a transformed box
between two untransformed siblings and measures that they do not move.
110 cases, fourteen categories, none dropped for engine disagreement — arbitrary-angle
rotations and skews included, which was the open risk, since those land off every engine's
pixel grid. The regression set is seeded from the union of every fixture's passes (102),
not the baseline's: the baseline returns the identity and so passes only the
rotate(180deg) cases, and a fixture that composes the list right to left instead of left
to right beat that comfortably and was admitted on the first run. One WEBENGINE_MODELS batch
over four gateway models then compiled 20 of 82 candidates, twelve of them at 110/110;
model_22 from qwen3.8-27b-fast is committed. Out of scope and stated in the module's
own doc: 3D functions and perspective, transform-box, the separate
translate/rotate/scale properties, a transformed box becoming a containing block or
a stacking context, painting, and scrollable overflow.
Inline atomic (crates/layout/src/inline_atomic.rs), the function that gives the
engine line boxes. Before it, boxtree.rs gave every computed display other than none
one block-level box, so a display: inline-block element stacked in normal flow like a
block and vertical-align, line-height and text-align reached nothing at all. Now a
container whose in-flow children are all inline-level establishes an inline formatting
context, fragment.rs lays each atomic inline out as a block formatting context root at
its explicit size, and the function decides where on which line each margin box goes, how
tall each line box is, where its baseline sits, and what the container's auto height
becomes.
Its answer is a set of positions rather than a single value — one placement per input box, plus the line boxes themselves — which makes it the first function here that can be wrong about one box and right about its neighbour on the same call, and the first that can panic on a well-typed input by indexing its own output. The runner catches that the way the transform runner does.
Scope is what makes it possible without text. In: line breaking between atomic inlines,
vertical-align in all six of its font-free forms, line-height as the strut,
text-align: left | right | center, margins/padding/borders on the atomic inlines, an
inline-block's own baseline (CSS 2.1 10.8.1's last-in-flow-line-box rule and its
bottom-margin-edge fallback), and the container's auto height as the sum of its line
boxes. Out, and stated in the module's own doc: text and every font-dependent value,
width: auto inline-blocks, a container mixing block-level and inline-level children,
floats and absolutely positioned boxes inside the inline formatting context,
text-align: justify, bidi, vertical writing modes, inline-table, inline-flex and
replaced elements.
The corpus's own design carries the weight. Every container sets font-size: 0 beside an
explicit line-height in px, so the strut's ascent and descent are zero, its baseline sits
at the middle of a known height, and vertical-align: middle aligns on that baseline
rather than on half an x-height nobody can measure. crates/harness/src/cases.rs already
emits no whitespace between tags, so no text node can appear between two atomic inlines,
and the case file asserts that for every case it produces. 111 cases over fifteen
categories, none dropped for engine disagreement — which is the answer to the open question
this corpus was built around: the three engines agree on a zero-size font's strut.
Admitted on the first batch at 111/111. Four candidates reached the whole corpus and each
came from a different one of the four gateway models, which makes this the third function
solved completely on its first batch, after floats and transforms. The two seeded-wrong
fixtures are worth more than the usual here: they are wrong about disjoint halves of the
function — one ignores vertical-align, the other never breaks a line — and between them
they pass all 111 cases, so the regression set covers the corpus from the first batch and
a candidate must pass everything to be admitted at all.
Table (crates/layout/src/table.rs), the fixed table layout, and the first function
whose structural half had to be built from nothing. Before it, tables did not exist in the
engine: boxtree.rs modelled no anonymous boxes and its own doc comment said so, a display: table-cell box was stacked like a block and filled its containing block's width, and
display: table was one of the keywords dom::computed::Display swept into its Other
catch-all. Now the box tree generates the anonymous rows and cells CSS 2.1 §17.2.1 calls for
inside a fixed-layout table, fragment.rs has a third formatting context alongside stacking
and line boxes, and the function decides every column's width and x offset, every row's
height and y offset, and the table's own used width and height.
It is the second function with two exported halves, after margin collapsing, and they are
split on the axis §17.5.2.1 splits the algorithm on: columns is answered from the table's
own width and the first row's cells alone, before any cell's content has been laid out, and
rows is answered afterwards from the heights that content produced. That order is the whole
content of the claim that a fixed table's horizontal layout does not depend on its cells.
Scope is what keeps it measurable. In: table-layout: fixed on a table with an explicit
width; columns from the first row's cells as a length, a percentage or auto, with auto
columns sharing what is left and a table wider than its columns spreading the difference;
one- and two-value border-spacing; padding and borders on the table and on cells under
either box-sizing; a cell's margins ignored; a row taller than its cells; and the two
anonymous table objects. Out, and stated in the module's own doc: table-layout: auto and
every table whose used layout is auto, column and column-group elements, row groups,
captions, colspan/rowspan, the collapsing-borders model, vertical-align other than
top, a table height larger than its rows, inline-table, and text.
No batch has been drawn. The committed candidate is the function's own deliberately wrong
baseline — every column the full width of the table, every cell stacked under the one before
it, which is exactly what the engine did with these boxes before the function existed —
committed as the starting candidate the way overflow and inline_atomic both were, so the
three layout* entry points default to something. The corpus, the runner, the three binaries
and the two seeded-wrong fixtures are all in place and compile; the build lane was
unreachable for the whole session, and both the browser oracle that fills the corpus and a
model batch refuse to run anywhere else. The structural half is checked instead against
hand-computed geometry in fragment.rs's own tests, which is what a browser would otherwise
have been asked.
Box generation, admitted by a feature job (specs/display-contents.md). qwen3.8-27b-fast-2's c6_qwen3_8_27b_fast_2_p017f6528 (seed 6), stored by the display-contents job after 0 repair round(s) at train 48 of 48 and held-out 15 of 15, passes 48 of 48 on the score check's corpus. The job's review passed it and its admit stage put it into the engine.
What the generated functions show #
- The generated part is small by construction. The hand-written walk decides which margins adjoin, which basis a percentage uses, which children stack; the model writes the leaf decision.
- The model has rarely beaten a hand-written answer. Where a hand fixture existed first, winners matched it except width, which added min/max clamping. Model-first functions (absolute, float, relpos, stacking, overflow, containing block, transform, box generation) were solved without any hand candidate.
- Building the scaffolding around a function costs more than generating it. Corpus, oracle, types and prompt
are hand work, and that cost is what decides the thesis (
docs/platform-economics.md→ "Per-feature cost"). - Common failure kinds: invented types, replies cut off before the code, one plausible misreading of the spec fixed by a single prompt sentence, and harness bugs that only a known-correct candidate exposes.
- A function's scope is a corpus decision before it is a code decision. The inline-atomic function exists at all because one shape of inline content — atomic inlines at explicit sizes — can be measured without a font, and the corpus is built to keep every font-dependent value out. Everything the three engines might disagree about was designed out before the first oracle run rather than dropped after it.