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 twelve 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.1–§4.4 preferred aspect ratio | model_34 (qwen3.8-27b-fast) |
109/109 | model-first |
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 got the same treatment: a baseline candidate that reproduces the hand-written rule it replaced, two or more seeded-wrong fixtures (deliberately broken candidates that the gate must reject — the proof that the corpus can tell right from wrong), then model batches.
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").
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 NEV_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.
Aspect ratio (crates/layout/src/aspect_ratio.rs), drawn from CSS Box Sizing 4 rather
than CSS 2.1, and the first function whose whole job is to transfer one size through a
ratio when either the width or the height was left auto. Before it, dom::computed had
no accessor for aspect-ratio at all, so a box with aspect-ratio: 2 / 1 and height: auto took its height from its children like any other block. The structural half is
hand-written — fragment.rs resolves padding, borders and the width and height equations
as before, then calls the function twice, once before the children are laid out (so a
height-determined box's width is known in time for its children) and once after (so the
automatic minimum size can grow the box to the children's extent).
The inputs are the ratio and whether auto was also given, both axes' box-sizing, the
box's own min/max sizes, its horizontal and vertical edges, the width the width equation
would give it, the children's extent from the stacking walk, whether overflow is
visible, and whether min-height was left auto. The outputs are content-box sizes. The
corpus keeps widths in px except where a case is about the automatic width, since the
width equation is a different function's job; crates/harness/src/aspect_ratio_cases.rs
asserts its own metadata and adds nothing to crates/harness/src/cases.rs's recipes, which
is what leaves fragment_check --random at 227/227.
baseline is the function's own deliberately wrong rule — the ratio is ignored and the
width and height equations' answers stand — committed as the starting candidate the way
table's was. content_box_always (the ratio always measured against the content box, and
always determining the height) and no_automatic_minimum (every rule but §4.3's automatic
minimum size) are the two seeded-wrong fixtures. aspect_ratio_generation_loop seeded the
permanent regression set with the 11 cases baseline already passes, and confirmed the
gate rejects both fixtures (content_box_always: 77/109, no_automatic_minimum: 97/109,
both admitted=false).
One mixed model batch of aspect_ratio_real_model_run then drew 82 candidates across
qwen3.8-flash-next (16), qwen3.8-27b-fast (32), qwen3.8-27b-fast-2 (32) and
deepseek-v4.1-flash (2); 74 compiled and scored. model_34 (qwen3.8-27b-fast) reached
109/109, every one of the thirteen categories at 100%, and admit::beats_committed
committed it to crates/layout/src/generated/aspect_ratio.rs. The loop then stopped
because no failing case remained. Its row lives in docs/model-first-cost.md →
"Aspect ratio". The substrate spec does not define a floor for this function in
bench/seam-floor.sh; a hand change after landing writes one, since the landing script
refuses a diff under bench/.
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) 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.